Browse Source

Merge remote-tracking branch 'origin' into 243-reverse-confidence-scores

pull/274/head
Diana Shkolnikov 9 years ago
parent
commit
1752155e0d
  1. 2
      README.md
  2. 13
      controller/place.js
  3. 13
      controller/search.js
  4. 1
      helper/geojsonify.js
  5. 6
      middleware/confidenceScore.js
  6. 4
      middleware/distance.js
  7. 70
      middleware/geocodeJSON.js
  8. 4
      middleware/renamePlacenames.js
  9. 4
      middleware/sendJSON.js
  10. 2
      package.json
  11. 19
      routes/v1.js
  12. 7
      sanitiser/_geo_reverse.js
  13. 32
      sanitiser/_id.js
  14. 38
      sanitiser/_size.js
  15. 4
      sanitiser/place.js
  16. 5
      sanitiser/reverse.js
  17. 21
      sanitiser/sanitizeAll.js
  18. 5
      sanitiser/search.js
  19. 4
      test/ciao/404.coffee
  20. 2
      test/ciao/CORS/headers_GET.coffee
  21. 10
      test/ciao/CORS/headers_OPTIONS.coffee
  22. 33
      test/ciao/autocomplete/basic_autocomplete.coffee
  23. 34
      test/ciao/autocomplete/null_island.coffee
  24. 2
      test/ciao/index.coffee
  25. 33
      test/ciao/place/basic_place.coffee
  26. 34
      test/ciao/place/missing_id.coffee
  27. 16
      test/ciao/place/msuccess.coffee
  28. 16
      test/ciao/place/success.coffee
  29. 34
      test/ciao/reverse/basic_reverse.coffee
  30. 34
      test/ciao/reverse/null_island.coffee
  31. 15
      test/ciao/reverse/success.coffee
  32. 41
      test/ciao/search/address_parsing.coffee
  33. 33
      test/ciao/search/basic_search.coffee
  34. 33
      test/ciao/search/no_params.coffee
  35. 35
      test/ciao/search/null_island.coffee
  36. 15
      test/ciao/search/success.coffee
  37. 6
      test/ciao/suggest/nearby_nobias.coffee
  38. 16
      test/ciao/suggest/success.coffee
  39. 16
      test/ciao/suggest/success_nearby.coffee
  40. 4
      test/unit/helper/geojsonify.js
  41. 24
      test/unit/middleware/distance.js
  42. 1
      test/unit/run.js
  43. 61
      test/unit/sanitiser/_size.js
  44. 135
      test/unit/sanitiser/place.js
  45. 144
      test/unit/sanitiser/reverse.js
  46. 170
      test/unit/sanitiser/search.js

2
README.md

@ -5,7 +5,7 @@
## Documentation
See our [API Documentation](https://github.com/pelias/api/blob/master/DOCS.md).
See our [API Documentation](https://github.com/pelias/api/blob/master/public/apiDoc.md).
## Install Dependencies

13
controller/place.js

@ -1,10 +1,18 @@
var service = { mget: require('../service/mget') };
function setup( backend ){
// allow overriding of dependencies
backend = backend || require('../src/backend');
function controller( req, res, next ){
// do not run controller when a request
// validation error has occurred.
if( req.errors && req.errors.length ){
return next();
}
var query = req.clean.ids.map( function(id) {
return {
_index: 'pelias',
@ -17,9 +25,8 @@ function setup( backend ){
// error handler
if( err ){ return next( err ); }
req.results = {
data: docs
};
// set response data
res.data = docs;
next();
});

13
controller/search.js

@ -8,6 +8,12 @@ function setup( backend, query ){
function controller( req, res, next ){
// do not run controller when a request
// validation error has occurred.
if( req.errors && req.errors.length ){
return next();
}
// backend command
var cmd = {
index: 'pelias',
@ -26,10 +32,9 @@ function setup( backend, query ){
// error handler
if( err ){ return next( err ); }
req.results = {
data: docs,
meta: meta
};
// set response data
res.data = docs;
res.meta = meta;
next();
});

1
helper/geojsonify.js

@ -8,7 +8,6 @@ var GeoJSON = require('geojson'),
var DETAILS_PROPS = [
'housenumber',
'street',
'category',
'postalcode',
'country_a',
'country',

6
middleware/confidenceScore.js

@ -23,17 +23,17 @@ function setup(peliasConfig) {
function computeScores(req, res, next) {
// do nothing if no result data set
if (!req.results || !req.results.data || !req.results.meta) {
if (!res || !res.data || !res.meta) {
return next();
}
// compute standard deviation and mean from all scores
var scores = req.results.meta.scores;
var scores = res.meta.scores;
var stdev = computeStandardDeviation(scores);
var mean = stats.mean(scores);
// loop through data items and determine confidence scores
req.results.data = req.results.data.map(computeConfidenceScore.bind(null, req, mean, stdev));
res.data = res.data.map(computeConfidenceScore.bind(null, req, mean, stdev));
next();
}

4
middleware/distance.js

@ -9,7 +9,7 @@ function setup() {
function computeDistances(req, res, next) {
// do nothing if no result data set
if (!req.results || !req.results.data) {
if (!res || !res.data) {
return next();
}
@ -17,7 +17,7 @@ function computeDistances(req, res, next) {
return next();
}
req.results.data.forEach(function (place) {
res.data.forEach(function (place) {
// the result of getDistance is in meters, so convert to kilometers
place.distance = geolib.getDistance(
{ latitude: req.clean.lat, longitude: req.clean.lon },

70
middleware/geocodeJSON.js

@ -1,71 +1,89 @@
var url = require('url');
var extend = require('extend');
var geojsonify = require('../helper/geojsonify').search;
function setup(peliasConfig) {
peliasConfig = peliasConfig || require( 'pelias-config' ).generate().api;
/**
* Returns a middleware function that converts elasticsearch
* results into geocodeJSON format.
*
* @param {object} [peliasConfig] api portion of pelias config
* @param {string} [basePath]
* @returns {middleware}
*/
function setup(peliasConfig, basePath) {
var opts = {
config: peliasConfig || require('pelias-config').generate().api,
basePath: basePath || '/'
};
function middleware(req, res, next) {
return convertToGeocodeJSON(peliasConfig, req, next);
return convertToGeocodeJSON(req, res, next, opts);
}
return middleware;
}
function convertToGeocodeJSON(peliasConfig, req, next) {
// do nothing if no result data set
if (!req.results || !req.results.data) {
return next();
}
req.results.geojson = { geocoding: {} };
/**
* Converts elasticsearch results into geocodeJSON format
*
* @param {object} req
* @param {object} res
* @param {object} next
* @param {object} opts
* @param {string} opts.basePath e.g. '/v1/'
* @param {string} opts.config.host e.g. 'pelias.mapzen.com'
* @param {string} opts.config.version e.g. 1.0
* @returns {*}
*/
function convertToGeocodeJSON(req, res, next, opts) {
res.body = { geocoding: {} };
// REQUIRED. A semver.org compliant version number. Describes the version of
// the GeocodeJSON spec that is implemented by this instance.
req.results.geojson.geocoding.version = '0.1';
res.body.geocoding.version = '0.1';
// OPTIONAL. Default: null. The attribution of the data. In case of multiple sources,
// and then multiple attributions, can be an object with one key by source.
// Can be a URI on the server, which outlines attribution details.
req.results.geojson.geocoding.attribution = peliasConfig.host + 'attribution';
res.body.geocoding.attribution = url.resolve(opts.config.host, opts.basePath + 'attribution');
// OPTIONAL. Default: null. The query that has been issued to trigger the
// search.
// Freeform object.
// This is the equivalent of how the engine interpreted the incoming request.
// Helpful for debugging and understanding how the input impacts results.
req.results.geojson.geocoding.query = req.clean;
res.body.geocoding.query = req.clean;
// OPTIONAL. Warnings and errors.
addMessages(req.results, 'warnings', req.results.geojson.geocoding);
addMessages(req.results, 'errors', req.results.geojson.geocoding);
addMessages(req, 'warnings', res.body.geocoding);
addMessages(req, 'errors', res.body.geocoding);
// OPTIONAL
// Freeform
addEngine(peliasConfig, req.results.geojson.geocoding);
addEngine(opts.config.version, res.body.geocoding);
// response envelope
req.results.geojson.geocoding.timestamp = new Date().getTime();
res.body.geocoding.timestamp = new Date().getTime();
// convert docs to geojson and merge with geocoding block
extend(req.results.geojson, geojsonify(req.results.data));
extend(res.body, geojsonify(res.data || []));
next();
}
function addMessages(results, msgType, geocoding) {
if (results.hasOwnProperty(msgType)) {
geocoding.messages = geocoding.messages || {};
geocoding.messages[msgType] = results[msgType];
function addMessages(req, msgType, geocoding) {
if (req.hasOwnProperty(msgType) && req[msgType].length) {
geocoding[msgType] = req[msgType];
}
}
function addEngine(peliasConfig, geocoding) {
function addEngine(version, geocoding) {
geocoding.engine = {
name: 'Pelias',
author: 'Mapzen',
version: peliasConfig.version
version: version
};
}

4
middleware/renamePlacenames.js

@ -32,12 +32,12 @@ function setup() {
function renamePlacenames(req, res, next) {
// do nothing if no result data set
if (!req.results || !req.results.data) {
if (!res || !res.data) {
return next();
}
// loop through data items and remap placenames
req.results.data = req.results.data.map(renameProperties);
res.data = res.data.map(renameProperties);
next();
}

4
middleware/sendJSON.js

@ -1,12 +1,12 @@
function sendJSONResponse(req, res, next) {
// do nothing if no result data set
if (!req.results || !req.results.geojson) {
if (!res || !res.body) {
return next();
}
// respond
return res.status(200).json(req.results.geojson);
return res.status(200).json(res.body);
}
module.exports = sendJSONResponse;

2
package.json

@ -59,7 +59,7 @@
"through2": "0.6.5"
},
"devDependencies": {
"ciao": "^0.3.4",
"ciao": "^0.6.0",
"istanbul": "^0.3.13",
"jshint": "^2.5.6",
"nsp": "^0.3.0",

19
routes/v1.js

@ -43,6 +43,8 @@ var postProc = {
*/
function addRoutes(app, peliasConfig) {
var base = '/v1/';
/** ------------------------- routers ------------------------- **/
var routers = {
@ -58,7 +60,7 @@ function addRoutes(app, peliasConfig) {
controllers.search(),
postProc.confidenceScores(peliasConfig),
postProc.renamePlacenames(),
postProc.geocodeJSON(peliasConfig),
postProc.geocodeJSON(peliasConfig, base),
postProc.sendJSON
]),
autocomplete: createRouter([
@ -67,7 +69,7 @@ function addRoutes(app, peliasConfig) {
controllers.search(null, require('../query/autocomplete')),
postProc.confidenceScores(peliasConfig),
postProc.renamePlacenames(),
postProc.geocodeJSON(peliasConfig),
postProc.geocodeJSON(peliasConfig, base),
postProc.sendJSON
]),
reverse: createRouter([
@ -79,14 +81,14 @@ function addRoutes(app, peliasConfig) {
// so it must be calculated first
postProc.confidenceScoresReverse(),
postProc.renamePlacenames(),
postProc.geocodeJSON(peliasConfig),
postProc.geocodeJSON(peliasConfig, base),
postProc.sendJSON
]),
place: createRouter([
sanitisers.place.middleware,
controllers.place(),
postProc.renamePlacenames(),
postProc.geocodeJSON(peliasConfig),
postProc.geocodeJSON(peliasConfig, base),
postProc.sendJSON
]),
status: createRouter([
@ -95,18 +97,19 @@ function addRoutes(app, peliasConfig) {
};
var base = '/v1/';
// api root
// static data endpoints
app.get ( base, routers.index );
app.get ( base + 'attribution', routers.attribution );
app.get ( '/attribution', routers.attribution );
app.get ( '/status', routers.status );
// backend dependent endpoints
app.get ( base + 'place', routers.place );
app.get ( base + 'autocomplete', routers.autocomplete );
app.get ( base + 'search', routers.search );
app.post( base + 'search', routers.search );
app.get ( base + 'reverse', routers.reverse );
app.get ( '/status', routers.status );
}
/**

7
sanitiser/_geo_reverse.js

@ -14,6 +14,13 @@ module.exports = function sanitize( raw, clean ){
geo_common.sanitize_coord( 'lat', clean, raw['point.lat'], LAT_LON_IS_REQUIRED );
geo_common.sanitize_coord( 'lon', clean, raw['point.lon'], LAT_LON_IS_REQUIRED );
// remove both if only one is set
// @todo: clean this up!
if( !clean.hasOwnProperty('lat') || !clean.hasOwnProperty('lon') ){
delete clean.lat;
delete clean.lon;
}
// boundary.circle.* is not mandatory, and only specifying radius is fine,
// as point.lat/lon will be used to fill those values by default
geo_common.sanitize_boundary_circle( clean, raw, CIRCLE_IS_REQUIRED, CIRCLE_MUST_BE_COMPLETE);

32
sanitiser/_id.js

@ -18,8 +18,18 @@ function sanitize( raw, clean ){
// error & warning messages
var messages = { errors: [], warnings: [] };
// 'raw.id' can be an array!?
var rawIds = check.array( raw.id ) ? _.unique( raw.id ) : [ raw.id ];
// 'raw.id' can be an array
var rawIds = check.array( raw.id ) ? _.unique( raw.id ) : [];
// 'raw.id' can be a string
if( check.unemptyString( raw.id ) ){
rawIds.push( raw.id );
}
// no ids provided
if( !rawIds.length ){
messages.errors.push( errorMessage('id') );
}
// ensure all elements are valid non-empty strings
rawIds = rawIds.filter( function( uc ){
@ -48,25 +58,27 @@ function sanitize( raw, clean ){
}
// id text
if( !check.unemptyString( id ) ){
else if( !check.unemptyString( id ) ){
messages.errors.push( errorMessage( rawId ) );
}
// type text
if( !check.unemptyString( type ) ){
else if( !check.unemptyString( type ) ){
messages.errors.push( errorMessage( rawId ) );
}
// type text must be one of the types
if( !_.contains( types, type ) ){
else if( !_.contains( types, type ) ){
messages.errors.push(
errorMessage('type', type + ' is invalid. It must be one of these values - [' + types.join(', ') + ']')
);
}
// add valid id to 'clean.ids' array
clean.ids.push({
id: id,
type: type
});
else {
clean.ids.push({
id: id,
type: type
});
}
});
return messages;

38
sanitiser/_size.js

@ -1,3 +1,4 @@
var check = require('check-types');
var MIN_SIZE = 1,
MAX_SIZE = 40,
@ -10,35 +11,22 @@ function sanitize( raw, clean ){
var messages = { errors: [], warnings: [] };
// coercions
var _size = parseInt( raw.size, 10 );
clean.size = parseInt( raw.size, 10 );
// invalid numeric input
// @todo: this can be removed now as queries have default sizes?
if( isNaN( _size ) ){
// set the default size
messages.warnings.push('invalid integer \'size\', using DEFAULT_SIZE');
if( isNaN( clean.size ) ){
clean.size = DEFAULT_SIZE;
}
// valid numeric input
else {
// ensure size falls within defined range
if( _size > MAX_SIZE ){
// set the max size
messages.warnings.push('out-of-range integer \'size\', using MAX_SIZE');
clean.size = MAX_SIZE;
}
else if( _size < MIN_SIZE ){
// set the min size
messages.warnings.push('out-of-range integer \'size\', using MIN_SIZE');
clean.size = MIN_SIZE;
}
else {
// set the input size
clean.size = _size;
}
// ensure size falls within defined range
else if( clean.size > MAX_SIZE ){
// set the max size
messages.warnings.push('out-of-range integer \'size\', using MAX_SIZE');
clean.size = MAX_SIZE;
}
else if( clean.size < MIN_SIZE ){
// set the min size
messages.warnings.push('out-of-range integer \'size\', using MIN_SIZE');
clean.size = MIN_SIZE;
}
return messages;

4
sanitiser/place.js

@ -14,10 +14,6 @@ module.exports.sanitiser_list = sanitizers;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
next();
});
};

5
sanitiser/reverse.js

@ -6,7 +6,6 @@ var sanitizeAll = require('../sanitiser/sanitizeAll'),
size: require('../sanitiser/_size'),
private: require('../sanitiser/_flag_bool')('private', false),
geo_reverse: require('../sanitiser/_geo_reverse'),
categories: require('../sanitiser/_categories'),
boundary_country: require('../sanitiser/_boundary_country'),
};
@ -19,10 +18,6 @@ module.exports.sanitiser_list = sanitizers;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
next();
});
};

21
sanitiser/sanitizeAll.js

@ -1,10 +1,16 @@
var check = require('check-types');
function sanitize( req, sanitizers, cb ){
// init an object to store clean
// (sanitized) input parameters
req.clean = {};
// init erros and warnings arrays
req.errors = [];
req.warnings = [];
// source of input parameters
// (in this case from the GET querystring params)
var params = req.query || {};
@ -12,14 +18,21 @@ function sanitize( req, sanitizers, cb ){
for (var s in sanitizers) {
var sanity = sanitizers[s]( params, req.clean );
// errors
// if errors occurred then set them
// on the req object.
if( sanity.errors.length ){
return cb( sanity.errors[0] );
req.errors = req.errors.concat( sanity.errors );
}
// if warnings occurred then set them
// on the req object.
if( sanity.warnings.length ){
req.warnings = req.warnings.concat( sanity.warnings );
}
}
return cb( undefined, req.clean );
// @todo remove these args, they do not need to be passed out
return cb( undefined, req.clean );
}
// export function

5
sanitiser/search.js

@ -7,7 +7,6 @@ var sanitizeAll = require('../sanitiser/sanitizeAll'),
sources: require('../sanitiser/_targets')('sources', require( '../query/sources' )),
private: require('../sanitiser/_flag_bool')('private', false),
geo_search: require('../sanitiser/_geo_search'),
categories: require('../sanitiser/_categories'),
boundary_country: require('../sanitiser/_boundary_country'),
};
@ -20,10 +19,6 @@ module.exports.sanitiser_list = sanitizers;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req, function( err, clean ){
if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
next();
});
};

4
test/ciao/404.coffee

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

2
test/ciao/cors.coffee → test/ciao/CORS/headers_GET.coffee

@ -4,6 +4,6 @@ path: '/'
#? access control headers correctly set
response.should.have.header 'Access-Control-Allow-Origin','*'
response.should.have.header 'Access-Control-Allow-Methods','GET'
response.should.have.header 'Access-Control-Allow-Methods','GET, OPTIONS'
response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type'
response.should.have.header 'Access-Control-Allow-Credentials','true'

10
test/ciao/CORS/headers_OPTIONS.coffee

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

33
test/ciao/autocomplete/basic_autocomplete.coffee

@ -0,0 +1,33 @@
#> basic autocomplete
path: '/v1/autocomplete?text=a'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['text'].should.eql 'a'
json.geocoding.query['size'].should.eql 10

34
test/ciao/autocomplete/null_island.coffee

@ -0,0 +1,34 @@
#> null island
path: '/v1/autocomplete?text=a&focus.point.lat=0&focus.point.lon=0'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['lat'].should.eql 0
json.geocoding.query['lon'].should.eql 0
json.geocoding.query['size'].should.eql 10

2
test/ciao/index.coffee

@ -3,7 +3,7 @@
path: '/v1/'
#? endpoint available
response.statusCode.should.equal 200
response.statusCode.should.be.equal 200
#? content-type header correctly set
response.should.have.header 'Content-Type','text/html; charset=utf-8'

33
test/ciao/place/basic_place.coffee

@ -0,0 +1,33 @@
#> basic place
path: '/v1/place?id=geoname:1'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['ids'].should.eql [{ id: '1', type: 'geoname' }]
should.not.exist json.geocoding.query['size']

34
test/ciao/place/missing_id.coffee

@ -0,0 +1,34 @@
#> missing id
path: '/v1/place'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.exist json.geocoding.errors
json.geocoding.errors.should.eql [ 'invalid param \'id\': text length, must be >0' ]
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['ids'].should.eql []
should.not.exist json.geocoding.query['size']

16
test/ciao/place/msuccess.coffee

@ -1,16 +0,0 @@
#> valid place query
path: '/v1/place?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

16
test/ciao/place/success.coffee

@ -1,16 +0,0 @@
#> valid place query
path: '/v1/place?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

34
test/ciao/reverse/basic_reverse.coffee

@ -0,0 +1,34 @@
#> basic reverse
path: '/v1/reverse?point.lat=1&point.lon=2'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['lat'].should.eql 1
json.geocoding.query['lon'].should.eql 2
json.geocoding.query['size'].should.eql 10

34
test/ciao/reverse/null_island.coffee

@ -0,0 +1,34 @@
#> null island
path: '/v1/reverse?point.lat=0&point.lon=0'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['lat'].should.eql 0
json.geocoding.query['lon'].should.eql 0
json.geocoding.query['size'].should.eql 10

15
test/ciao/reverse/success.coffee

@ -1,15 +0,0 @@
#> valid reverse query
path: '/v1/reverse?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

41
test/ciao/search/address_parsing.coffee

@ -0,0 +1,41 @@
#> address parsing
path: '/v1/search?text=30%20w%2026th%20st%2C%20ny'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['text'].should.eql '30 w 26th st, ny'
json.geocoding.query['size'].should.eql 10
#? address parsing
json.geocoding.query.parsed_text['name'].should.eql '30 w 26th st'
json.geocoding.query.parsed_text['number'].should.eql 30
json.geocoding.query.parsed_text['street'].should.eql 'w 26th st'
json.geocoding.query.parsed_text['state'].should.eql 'NY'
json.geocoding.query.parsed_text['regions'].should.eql []
json.geocoding.query.parsed_text['admin_parts'].should.eql "ny"

33
test/ciao/search/basic_search.coffee

@ -0,0 +1,33 @@
#> basic search
path: '/v1/search?text=a'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['text'].should.eql 'a'
json.geocoding.query['size'].should.eql 10

33
test/ciao/search/no_params.coffee

@ -0,0 +1,33 @@
#> no params specified
path: '/v1/search'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.exist json.geocoding.errors
json.geocoding.errors.should.eql [ 'invalid param \'text\': text length, must be >0' ]
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['size'].should.eql 10

35
test/ciao/search/null_island.coffee

@ -0,0 +1,35 @@
#> null island
path: '/v1/search?text=a&focus.point.lat=0&focus.point.lon=0'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? inputs
json.geocoding.query['text'].should.eql 'a'
json.geocoding.query['size'].should.eql 10
json.geocoding.query['lat'].should.eql 0
json.geocoding.query['lon'].should.eql 0

15
test/ciao/search/success.coffee

@ -1,15 +0,0 @@
#> valid search query
path: '/v1/search?text=lake&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

6
test/ciao/suggest/nearby_nobias.coffee

@ -1,6 +0,0 @@
#> suggest without geo bias
path: '/v1/suggest/nearby?text=a'
#? 400 bad request
response.statusCode.should.equal 400

16
test/ciao/suggest/success.coffee

@ -1,16 +0,0 @@
#> valid suggest query
path: '/v1/suggest?text=a&lat=0&lon=0'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
json.date.should.be.within now-5000, now+5000
#? valid geojson
json.type.should.equal 'FeatureCollection'
json.features.should.be.instanceof Array

16
test/ciao/suggest/success_nearby.coffee

@ -1,16 +0,0 @@
#> valid suggest query
path: '/v1/suggest/nearby?text=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

4
test/unit/helper/geojsonify.js

@ -153,7 +153,6 @@ module.exports.tests.search = function(test, common) {
'localadmin': 'test1',
'locality': 'test2',
'neighbourhood': 'test3',
'category': ['food', 'nightlife'],
'housenumber': '13',
'street': 'Liverpool Road',
'postalcode': 'N1 0RW'
@ -206,8 +205,7 @@ module.exports.tests.search = function(test, common) {
'county': 'New York',
'localadmin': 'Manhattan',
'locality': 'New York',
'neighbourhood': 'Koreatown',
'category': ['tourism', 'transport']
'neighbourhood': 'Koreatown'
}
}
]

24
test/unit/middleware/distance.js

@ -8,22 +8,22 @@ module.exports.tests.computeDistance = function(test, common) {
clean: {
lat: 45,
lon: -77
},
results: {
data: [
{
center_point: {
lat: 40,
lon: -71
}
}
]
}
};
var res = {
data: [
{
center_point: {
lat: 40,
lon: -71
}
}
]
};
var expected = 742.348;
distance(req, null, function () {
t.equal(req.results.data[0].distance, expected, 'correct distance computed');
distance(req, res, function () {
t.equal(res.data[0].distance, expected, 'correct distance computed');
t.end();
});
});

1
test/unit/run.js

@ -28,6 +28,7 @@ var tests = [
require('./sanitiser/_geo_common'),
require('./middleware/distance'),
require('./middleware/confidenceScoreReverse'),
require('./sanitiser/_size'),
];
tests.map(function(t) {

61
test/unit/sanitiser/_size.js

@ -0,0 +1,61 @@
var sanitize = require('../../../sanitiser/_size');
module.exports.tests = {};
module.exports.tests.sanitize_size = function(test, common) {
test('size=0', function(t) {
var raw = { size: 0 };
var clean = {};
var res = sanitize(raw, clean);
t.equal(res.errors.length, 0, 'should return no errors');
t.equal(res.warnings.length, 1, 'should return warning');
t.equal(res.warnings[0], 'out-of-range integer \'size\', using MIN_SIZE', 'check warning text');
t.equal(clean.size, 1, 'default to 1');
t.end();
});
test('size=10000', function(t) {
var raw = { size: 10000 };
var clean = {};
var res = sanitize(raw, clean);
t.equal(res.errors.length, 0, 'should return no errors');
t.equal(res.warnings.length, 1, 'should return warning');
t.equal(res.warnings[0], 'out-of-range integer \'size\', using MAX_SIZE', 'check warning text');
t.equal(clean.size, 40, 'default to 40');
t.end();
});
test('size not set', function(t) {
var raw = {};
var clean = {};
var res = sanitize(raw, clean);
t.equal(res.errors.length, 0, 'should return no errors');
t.equal(res.warnings.length, 0, 'should return no warning');
t.equal(clean.size, 10, 'default to 10');
t.end();
});
var valid_sizes = [5, '5', 5.5, '5.5'];
valid_sizes.forEach(function (size) {
test('size=' + size, function (t) {
var raw = {size: size};
var clean = {};
var res = sanitize(raw, clean);
t.equal(res.errors.length, 0, 'should return no errors');
t.equal(res.warnings.length, 0, 'should return warning');
t.equal(clean.size, 5, 'set to correct integer');
t.end();
});
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE _size ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

135
test/unit/sanitiser/place.js

@ -1,6 +1,8 @@
// @todo: refactor this test, it's pretty messy, brittle and hard to follow
var place = require('../../../sanitiser/place'),
_sanitize = place.sanitize,
sanitize = place.sanitize,
middleware = place.middleware,
types = require('../../../query/types'),
delimiter = ':',
@ -11,12 +13,14 @@ var place = require('../../../sanitiser/place'),
var type = input.split(delimiter)[0];
return type + ' is invalid. It must be one of these values - [' + types.join(', ') + ']'; },
defaultClean = { ids: [ { id: '123', type: 'geoname' } ], private: false },
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' ]
};
// these are the default values you would expect when no input params are specified.
var emptyClean = { ids: [], private: false };
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
@ -33,33 +37,35 @@ module.exports.tests.interface = function(test, common) {
};
module.exports.tests.sanitize_id = function(test, common) {
test('invalid input', function(t) {
test('id: invalid input', function(t) {
inputs.invalid.forEach( function( input ){
sanitize({ id: input }, function( err, clean ){
switch (err) {
var req = { query: { id: input } };
sanitize(req, function( ){
switch (req.errors[0]) {
case defaultError:
t.equal(err, defaultError, input + ' is invalid input'); break;
t.equal(req.errors[0], defaultError, input + ' is invalid input'); break;
case defaultLengthError(input):
t.equal(err, defaultLengthError(input), input + ' is invalid (missing id/type)'); break;
t.equal(req.errors[0], defaultLengthError(input), input + ' is invalid (missing id/type)'); break;
case defaultFormatError:
t.equal(err, defaultFormatError, input + ' is invalid (invalid format)'); break;
t.equal(req.errors[0], defaultFormatError, input + ' is invalid (invalid format)'); break;
case defaultMissingTypeError(input):
t.equal(err, defaultMissingTypeError(input), input + ' is an unknown type'); break;
t.equal(req.errors[0], defaultMissingTypeError(input), input + ' is an unknown type'); break;
default: break;
}
t.equal(clean, undefined, 'clean not set');
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
});
t.end();
});
test('valid input', function(t) {
test('id: 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] } ], private: false };
sanitize({ id: input }, function( err, clean ){
t.equal(err, undefined, 'no error (' + input + ')' );
t.deepEqual(clean, expected, 'clean set correctly (' + input + ')');
var req = { query: { id: input } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no error (' + input + ')' );
t.deepEqual( req.clean, expected, 'clean set correctly (' + input + ')');
});
});
t.end();
@ -68,26 +74,26 @@ module.exports.tests.sanitize_id = function(test, common) {
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');
test('ids: invalid input', function(t) {
var req = { query: { id: inputs.invalid } };
var expected = [
'invalid param \'id\': text length, must be >0',
'invalid param \':\': text length, must be >0',
'invalid param \'::\': text length, must be >0',
'invalid param \'geoname:\': text length, must be >0',
'invalid param \':234\': text length, must be >0',
'gibberish is invalid. It must be one of these values - ' +
'[geoname, osmnode, osmway, admin0, admin1, admin2, neighborhood, ' +
'locality, local_admin, osmaddress, openaddresses]'
];
sanitize(req, function(){
t.deepEqual(req.errors, expected);
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
t.end();
});
test('valid input', function(t) {
test('ids: valid input', function(t) {
var expected={};
expected.ids = [];
inputs.valid.forEach( function( input ){
@ -95,9 +101,10 @@ module.exports.tests.sanitize_ids = function(test, common) {
expected.ids.push({ id: input_parts[1], type: input_parts[0] });
});
expected.private = false;
sanitize({ id: inputs.valid }, function( err, clean ){
t.equal(err, undefined, 'no error' );
t.deepEqual(clean, expected, 'clean set correctly');
var req = { query: { id: inputs.valid } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.clean, expected, 'clean set correctly' );
});
t.end();
});
@ -107,8 +114,11 @@ module.exports.tests.sanitize_private = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(value) {
test('invalid private param ' + value, function(t) {
sanitize({ id:'geoname:123', 'private': value}, function( err, clean ){
t.equal(clean.private, false, 'default private set (to false)');
var req = { query: { id:'geoname:123', 'private': value } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.equal(req.clean.private, false, 'default private set (to false)');
t.end();
});
});
@ -117,8 +127,11 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_values = ['true', true, 1];
valid_values.forEach(function(value) {
test('valid private param ' + value, function(t) {
sanitize({ id:'geoname:123', 'private': value }, function( err, clean ){
t.equal(clean.private, true, 'private set to true');
var req = { query: { id:'geoname:123', 'private': value } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.equal(req.clean.private, true, 'private set to true');
t.end();
});
});
@ -127,16 +140,22 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(value) {
test('test setting false explicitly ' + value, function(t) {
sanitize({ id:'geoname:123', 'private': value }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { id:'geoname:123', 'private': value } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.equal(req.clean.private, false, 'private set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ id:'geoname:123' }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { id:'geoname:123' } };
sanitize(req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.equal(req.clean.private, false, 'private set to false');
t.end();
});
});
@ -144,10 +163,12 @@ module.exports.tests.sanitize_private = function(test, common) {
module.exports.tests.de_dupe = function(test, common) {
var expected = { ids: [ { id: '1', type: 'geoname' }, { id: '2', type: 'osmnode' } ], private: false };
var req = { query: { id: ['geoname:1', 'osmnode:2', 'geoname:1'] } };
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');
sanitize( req, function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.deepEqual(req.clean, expected, 'clean set correctly');
t.end();
});
});
@ -155,31 +176,21 @@ module.exports.tests.de_dupe = function(test, common) {
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');
var req = { query: {} };
sanitize( req, function(){
t.equal( req.errors[0], defaultError, 'handle invalid params gracefully');
t.deepEqual( req.warnings, [], 'no warnings' );
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');
var req = { query: { id: 'geoname:123' }};
var next = function(){
t.deepEqual( req.errors, [], 'no errors' );
t.deepEqual( req.warnings, [], 'no warnings' );
t.deepEqual(req.clean, defaultClean);
t.end();
};

144
test/unit/sanitiser/reverse.js

@ -1,6 +1,8 @@
// @todo: refactor this test, it's pretty messy, brittle and hard to follow
var reverse = require('../../../sanitiser/reverse'),
_sanitize = reverse.sanitize,
sanitize = reverse.sanitize,
middleware = reverse.middleware,
defaultError = 'missing param \'lat\'',
defaultClean = { lat:0,
@ -9,10 +11,12 @@ var reverse = require('../../../sanitiser/reverse'),
lon: 0,
size: 10,
private: false,
categories: [],
boundary: { }
},
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
};
// these are the default values you would expect when no input params are specified.
// @todo: why is this different from $defaultClean?
var emptyClean = { boundary: {}, private: false, size: 10, types: {} };
module.exports.tests = {};
@ -31,7 +35,7 @@ module.exports.tests.interface = function(test, common) {
module.exports.tests.sanitisers = function(test, common) {
test('check sanitiser list', function (t) {
var expected = ['layers', 'sources', 'size', 'private', 'geo_reverse', 'categories', 'boundary_country'];
var expected = ['layers', 'sources', 'size', 'private', 'geo_reverse', 'boundary_country'];
t.deepEqual(Object.keys(reverse.sanitiser_list), expected);
t.end();
});
@ -45,29 +49,32 @@ module.exports.tests.sanitize_lat = function(test, common) {
};
test('invalid lat', function(t) {
lats.invalid.forEach( function( lat ){
sanitize({ 'point.lat': lat, 'point.lon': 0 }, function( err, clean ){
t.equal(err, 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude');
t.equal(clean, undefined, 'clean not set');
var req = { query: { 'point.lat': lat, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.errors[0], 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude');
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
});
t.end();
});
test('valid lat', function(t) {
lats.valid.forEach( function( lat ){
sanitize({ 'point.lat': lat, 'point.lon': 0 }, function( err, clean ){
var req = { query: { 'point.lat': lat, 'point.lon': 0 } };
sanitize(req, function(){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lat = parseFloat( lat );
t.equal(err, undefined, 'no error');
t.equal(clean.lat, parseFloat(lat), 'clean set correctly (' + lat + ')');
t.deepEqual(req.errors, [], 'no errors');
t.equal(req.clean.lat, parseFloat(lat), 'clean set correctly (' + lat + ')');
});
});
t.end();
});
test('missing lat', function(t) {
lats.missing.forEach( function( lat ){
sanitize({ 'point.lat': lat, 'point.lon': 0 }, function( err, clean ){
t.equal(err, 'missing param \'lat\'', 'latitude is a required field');
t.equal(clean, undefined, 'clean not set');
var req = { query: { 'point.lat': lat, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.errors[0], 'missing param \'lat\'', 'latitude is a required field');
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
});
t.end();
@ -81,43 +88,50 @@ module.exports.tests.sanitize_lon = function(test, common) {
};
test('valid lon', function(t) {
lons.valid.forEach( function( lon ){
sanitize({ 'point.lat': 0, 'point.lon': lon }, function( err, clean ){
var req = { query: { 'point.lat': 0, 'point.lon': lon } };
sanitize(req, function(){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lon = parseFloat( lon );
t.equal(err, undefined, 'no error');
t.equal(clean.lon, parseFloat(lon), 'clean set correctly (' + lon + ')');
t.deepEqual(req.errors, [], 'no errors');
t.equal(req.clean.lon, parseFloat(lon), 'clean set correctly (' + lon + ')');
});
});
t.end();
});
test('missing lon', function(t) {
lons.missing.forEach( function( lon ){
sanitize({ 'point.lat': 0, 'point.lon': lon }, function( err, clean ){
t.equal(err, 'missing param \'lon\'', 'longitude is a required field');
t.equal(clean, undefined, 'clean not set');
var req = { query: { 'point.lat': 0, 'point.lon': lon } };
// @todo: why is lat set?
var expected = { boundary: {}, lat: 0, private: false, size: 10, types: {} };
sanitize(req, function(){
t.equal(req.errors[0], 'missing param \'lon\'', 'longitude is a required field');
t.deepEqual(req.clean, expected, 'clean only has default values set');
});
});
t.end();
});
};
module.exports.tests.sanitize_size = function(test, common) {
test('invalid size value', function(t) {
sanitize({ size: 'a', 'point.lat': 0, 'point.lon': 0 }, function( err, clean ){
t.equal(clean.size, 10, 'default size set');
var req = { query: { size: 'a', 'point.lat': 0, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 10, 'default size set');
t.end();
});
});
test('below min size value', function(t) {
sanitize({ size: -100, 'point.lat': 0, 'point.lon': 0 }, function( err, clean ){
t.equal(clean.size, 1, 'min size set');
var req = { query: { size: -100, 'point.lat': 0, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 1, 'min size set');
t.end();
});
});
test('above max size value', function(t) {
sanitize({ size: 9999, 'point.lat': 0, 'point.lon': 0 }, function( err, clean ){
t.equal(clean.size, 40, 'max size set');
var req = { query: { size: 9999, 'point.lat': 0, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 40, 'max size set');
t.end();
});
});
@ -127,8 +141,9 @@ module.exports.tests.sanitize_private = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(value) {
test('invalid private param ' + value, function(t) {
sanitize({ 'point.lat': 0, 'point.lon': 0, 'private': value}, function( err, clean ){
t.equal(clean.private, false, 'default private set (to false)');
var req = { query: { 'point.lat': 0, 'point.lon': 0, 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'default private set (to false)');
t.end();
});
});
@ -137,8 +152,9 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_values = ['true', true, 1, '1'];
valid_values.forEach(function(value) {
test('valid private param ' + value, function(t) {
sanitize({ 'point.lat': 0, 'point.lon': 0, 'private': value }, function( err, clean ){
t.equal(clean.private, true, 'private set to true');
var req = { query: { 'point.lat': 0, 'point.lon': 0, 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, true, 'private set to true');
t.end();
});
});
@ -147,78 +163,28 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(value) {
test('test setting false explicitly ' + value, function(t) {
sanitize({ 'point.lat': 0, 'point.lon': 0, 'private': value }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { 'point.lat': 0, 'point.lon': 0, 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ 'point.lat': 0, 'point.lon': 0 }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
t.end();
});
});
};
module.exports.tests.sanitize_categories = function(test, common) {
var queryParams = { 'point.lat': 0, 'point.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');
var req = { query: { 'point.lat': 0, 'point.lon': 0 } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
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 ){
t.equal(code, 400, 'status set');
}};
var next = function( message ){
t.equals(message, defaultError);
t.end();
};
middleware( {}, res, next );
});
};
module.exports.tests.middleware_success = function(test, common) {
test('middleware success', function(t) {
var req = { query: { 'point.lat': 0, 'point.lon': 0 }};
var next = function( message ){
t.equal(message, undefined, 'no error message set');
var next = function(){
t.deepEqual(req.errors, [], 'no error message set');
t.deepEqual(req.clean, defaultClean);
t.end();
};

170
test/unit/sanitiser/search.js

@ -3,7 +3,7 @@ var search = require('../../../sanitiser/search'),
_text = require('../sanitiser/_text'),
parser = require('../../../helper/query_parser'),
defaultParsed = _text.defaultParsed,
_sanitize = search.sanitize,
sanitize = search.sanitize,
middleware = search.middleware,
defaultError = 'invalid param \'text\': text length, must be >0',
defaultClean = { text: 'test',
@ -11,8 +11,11 @@ var search = require('../../../sanitiser/search'),
},
size: 10,
parsed_text: defaultParsed,
},
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
};
// these are the default values you would expect when no input params are specified.
// @todo: why is this different from $defaultClean?
var emptyClean = { boundary: {}, private: false, size: 10, types: {} };
module.exports.tests = {};
@ -31,7 +34,7 @@ module.exports.tests.interface = function(test, common) {
module.exports.tests.sanitisers = function(test, common) {
test('check sanitiser list', function (t) {
var expected = ['text', 'size', 'layers', 'sources', 'private', 'geo_search', 'categories', 'boundary_country' ];
var expected = ['text', 'size', 'layers', 'sources', 'private', 'geo_search', 'boundary_country' ];
t.deepEqual(Object.keys(search.sanitiser_list), expected);
t.end();
});
@ -41,9 +44,10 @@ module.exports.tests.sanitize_invalid_text = function(test, common) {
test('invalid text', function(t) {
var invalid = [ '', 100, null, undefined, new Date() ];
invalid.forEach( function( text ){
sanitize({ text: text }, function( err, clean ){
t.equal(err, 'invalid param \'text\': text length, must be >0', text + ' is an invalid text');
t.equal(clean, undefined, 'clean not set');
var req = { query: { text: text } };
sanitize(req, function(){
t.equal(req.errors[0], 'invalid param \'text\': text length, must be >0', text + ' is an invalid text');
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
});
t.end();
@ -52,22 +56,25 @@ module.exports.tests.sanitize_invalid_text = function(test, common) {
module.exports.tests.sanitise_valid_text = function(test, common) {
test('valid short text', function(t) {
sanitize({ text: 'a' }, function( err, clean ){
t.equal(err, undefined, 'no error');
var req = { query: { text: 'a' } };
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
});
t.end();
});
test('valid not-quite-as-short text', function(t) {
sanitize({ text: 'aa' }, function( err, clean ){
t.equal(err, undefined, 'no error');
var req = { query: { text: 'aa' } };
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
});
t.end();
});
test('valid longer text', function(t) {
sanitize({ text: 'aaaaaaaa' }, function( err, clean ){
t.equal(err, undefined, 'no error');
var req = { query: { text: 'aaaaaaaa' } };
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
});
t.end();
});
@ -78,13 +85,14 @@ module.exports.tests.sanitize_text_with_delim = function(test, common) {
test('valid texts with a comma', function(t) {
texts.forEach( function( text ){
sanitize({ text: text }, function( err, clean ){
var req = { query: { text: text } };
sanitize(req, function(){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.text = text;
expected.parsed_text = parser.get_parsed_address(text);
t.equal(err, undefined, 'no error');
t.equal(clean.parsed_text.name, expected.parsed_text.name, 'clean name set correctly');
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean.parsed_text.name, expected.parsed_text.name, 'clean name set correctly');
});
});
@ -94,8 +102,9 @@ module.exports.tests.sanitize_text_with_delim = function(test, common) {
module.exports.tests.sanitize_private_no_value = function(test, common) {
test('default private should be set to true', function(t) {
sanitize({ text: 'test' }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { text: 'test' } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
});
t.end();
});
@ -103,8 +112,9 @@ module.exports.tests.sanitize_private_no_value = function(test, common) {
module.exports.tests.sanitize_private_explicit_true_value = function(test, common) {
test('explicit private should be set to true', function(t) {
sanitize({ text: 'test', private: true }, function( err, clean ){
t.equal(clean.private, true, 'private set to true');
var req = { query: { text: 'test', private: true } };
sanitize(req, function(){
t.equal(req.clean.private, true, 'private set to true');
});
t.end();
});
@ -112,8 +122,9 @@ module.exports.tests.sanitize_private_explicit_true_value = function(test, commo
module.exports.tests.sanitize_private_explicit_false_value = function(test, common) {
test('explicit private should be set to false', function(t) {
sanitize({ text: 'test', private: false }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { text: 'test', private: false } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
});
t.end();
});
@ -126,19 +137,21 @@ module.exports.tests.sanitize_lat = function(test, common) {
};
test('invalid lat', function(t) {
lats.invalid.forEach( function( lat ){
sanitize({ text: 'test', 'focus.point.lat': lat, 'focus.point.lon': 0 }, function( err, clean ){
t.equal(err, 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude');
t.equal(clean, undefined, 'clean not set');
var req = { query: { text: 'test', 'focus.point.lat': lat, 'focus.point.lon': 0 } };
sanitize(req, function(){
t.equal(req.errors[0], 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude');
t.deepEqual(req.clean, emptyClean, 'clean only has default values set');
});
});
t.end();
});
test('valid lat', function(t) {
lats.valid.forEach( function( lat ){
sanitize({ text: 'test', 'focus.point.lat': lat, 'focus.point.lon': 0 }, function( err, clean ){
var req = { query: { text: 'test', 'focus.point.lat': lat, 'focus.point.lon': 0 } };
sanitize(req, function(){
var expected_lat = parseFloat( lat );
t.equal(err, undefined, 'no error');
t.deepEqual(clean.lat, expected_lat, 'clean lat set correctly (' + lat + ')');
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean.lat, expected_lat, 'clean lat set correctly (' + lat + ')');
});
});
t.end();
@ -151,11 +164,12 @@ module.exports.tests.sanitize_lon = function(test, common) {
};
test('valid lon', function(t) {
lons.valid.forEach( function( lon ){
sanitize({ text: 'test', 'focus.point.lat': 0, 'focus.point.lon': lon }, function( err, clean ){
var req = { query: { text: 'test', 'focus.point.lat': 0, 'focus.point.lon': lon } };
sanitize(req, function(){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lon = parseFloat( lon );
t.equal(err, undefined, 'no error');
t.deepEqual(clean.lon, expected.lon, 'clean set correctly (' + lon + ')');
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean.lon, expected.lon, 'clean set correctly (' + lon + ')');
});
});
t.end();
@ -164,26 +178,29 @@ module.exports.tests.sanitize_lon = function(test, common) {
module.exports.tests.sanitize_optional_geo = function(test, common) {
test('no lat/lon', function(t) {
sanitize({ text: 'test' }, function( err, clean ){
t.equal(err, undefined, 'no error');
t.equal(clean.lat, undefined, 'clean set without lat');
t.equal(clean.lon, undefined, 'clean set without lon');
var req = { query: { text: 'test' } };
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean.lat, undefined, 'clean set without lat');
t.equal(req.clean.lon, undefined, 'clean set without lon');
});
t.end();
});
test('no lat', function(t) {
sanitize({ text: 'test', 'focus.point.lon': 0 }, function( err, clean ){
var req = { query: { text: 'test', 'focus.point.lon': 0 } };
sanitize(req, function(){
var expected_lon = 0;
t.equal(err, undefined, 'no error');
t.deepEqual(clean.lon, expected_lon, 'clean set correctly (without any lat)');
t.equal(req.errors[0], undefined, 'no error');
t.deepEqual(req.clean.lon, expected_lon, 'clean set correctly (without any lat)');
});
t.end();
});
test('no lon', function(t) {
sanitize({ text: 'test', 'focus.point.lat': 0 }, function( err, clean ){
var req = { query: { text: 'test', 'focus.point.lat': 0 } };
sanitize(req, function(){
var expected_lat = 0;
t.equal(err, undefined, 'no error');
t.deepEqual(clean.lat, expected_lat, 'clean set correctly (without any lon)');
t.equal(req.errors[0], undefined, 'no error');
t.deepEqual(req.clean.lat, expected_lat, 'clean set correctly (without any lon)');
});
t.end();
});
@ -219,18 +236,20 @@ module.exports.tests.sanitize_bbox = function(test, common) {
};
test('invalid bbox', function(t) {
bboxes.invalid.forEach( function( bbox ){
sanitize({ text: 'test', bbox: bbox }, function( err, clean ){
t.equal(err, undefined, 'no error');
t.equal(clean.bbox, undefined, 'falling back on 50km distance from centroid');
var req = { query: { text: 'test', bbox: bbox } };
sanitize(req, function(){
t.equal(req.errors[0], undefined, 'no error');
t.equal(req.clean.bbox, undefined, 'falling back on 50km distance from centroid');
});
});
t.end();
});
test('valid bbox', function(t) {
bboxes.valid.forEach( function( bbox ){
sanitize({ text: 'test', bbox: bbox }, function( err, clean ){
var req = { query: { text: 'test', bbox: bbox } };
sanitize(req, function(){
var bboxArray = bbox.split(',').map(function(i) {
return parseInt(i);
return parseInt(i, 10);
});
var expected_bbox = {
right: Math.max(bboxArray[0], bboxArray[2]),
@ -238,8 +257,8 @@ module.exports.tests.sanitize_bbox = function(test, common) {
left: Math.min(bboxArray[0], bboxArray[2]),
bottom: Math.min(bboxArray[1], bboxArray[3])
};
t.equal(err, undefined, 'no error');
t.deepEqual(clean.bbox, expected_bbox, 'clean set correctly (' + bbox + ')');
t.equal(req.errors[0], undefined, 'no error');
t.deepEqual(req.clean.bbox, expected_bbox, 'clean set correctly (' + bbox + ')');
});
});
t.end();
@ -248,20 +267,23 @@ module.exports.tests.sanitize_bbox = function(test, common) {
module.exports.tests.sanitize_size = function(test, common) {
test('invalid size value', function(t) {
sanitize({ size: 'a', text: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 10, 'default size set');
var req = { query: { size: 'a', text: 'test', lat: 0, lon: 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 10, 'default size set');
t.end();
});
});
test('below min size value', function(t) {
sanitize({ size: -100, text: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 1, 'min size set');
var req = { query: { size: -100, text: 'test', lat: 0, lon: 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 1, 'min size set');
t.end();
});
});
test('above max size value', function(t) {
sanitize({ size: 9999, text: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 40, 'max size set');
var req = { query: { size: 9999, text: 'test', lat: 0, lon: 0 } };
sanitize(req, function(){
t.equal(req.clean.size, 40, 'max size set');
t.end();
});
});
@ -271,8 +293,9 @@ module.exports.tests.sanitize_private = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(value) {
test('invalid private param ' + value, function(t) {
sanitize({ text: 'test', lat: 0, lon: 0, 'private': value }, function( err, clean ){
t.equal(clean.private, false, 'default private set (to false)');
var req = { query: { text: 'test', lat: 0, lon: 0, 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'default private set (to false)');
t.end();
});
});
@ -281,8 +304,9 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_values = ['true', true, 1, '1'];
valid_values.forEach(function(value) {
test('valid private ' + value, function(t) {
sanitize({ text: 'test', 'private': value}, function( err, clean ){
t.equal(clean.private, true, 'private set to true');
var req = { query: { text: 'test', 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, true, 'private set to true');
t.end();
});
});
@ -291,16 +315,18 @@ module.exports.tests.sanitize_private = function(test, common) {
var valid_false_values = ['false', false, 0, '0'];
valid_false_values.forEach(function(value) {
test('test setting false explicitly ' + value, function(t) {
sanitize({ text: 'test', 'private': value }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { text: 'test', 'private': value } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ text: 'test' }, function( err, clean ){
t.equal(clean.private, false, 'private set to false');
var req = { query: { text: 'test' } };
sanitize(req, function(){
t.equal(req.clean.private, false, 'private set to false');
t.end();
});
});
@ -308,31 +334,19 @@ module.exports.tests.sanitize_private = function(test, common) {
module.exports.tests.invalid_params = function(test, common) {
test('invalid text params', function(t) {
sanitize( undefined, function( err, clean ){
t.equal(err, defaultError, 'handle invalid params gracefully');
var req = { query: {} };
sanitize( req, function(){
t.equal(req.errors[0], 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: { text: 'test' }};
var next = function( message ){
t.equal(message, undefined, 'no error message set');
t.deepEqual(req.errors, [], 'no error messages set');
t.end();
};
middleware( req, undefined, next );

Loading…
Cancel
Save