Browse Source

added support for coarse reverse geocoding

several things are in this commit:

- coarse reverse controller / pip service
- inject "conditional execute" predicate into search service to shortcut execution conditions
- added coarse reverse controller to precede standard reverse controller
- lots of tests!
pull/810/head
Stephen Hess 8 years ago
parent
commit
52a6ec68ff
  1. 129
      controller/coarse_reverse.js
  2. 5
      controller/predicates/has_data.js
  3. 5
      controller/predicates/has_errors.js
  4. 7
      controller/predicates/is_coarse_reverse.js
  5. 21
      controller/search.js
  6. 3
      package.json
  7. 29
      routes/v1.js
  8. 33
      service/pointinpolygon.js
  9. 578
      test/unit/controller/coarse_reverse.js
  10. 2
      test/unit/controller/place.js
  11. 60
      test/unit/controller/predicates/has_data.js
  12. 60
      test/unit/controller/predicates/has_errors.js
  13. 128
      test/unit/controller/predicates/is_coarse_reverse.js
  14. 58
      test/unit/controller/search.js
  15. 7
      test/unit/run.js
  16. 140
      test/unit/service/pointinpolygon.js

129
controller/coarse_reverse.js

@ -0,0 +1,129 @@
const logger = require('pelias-logger').get('coarse_reverse');
const _ = require('lodash');
const Document = require('pelias-model').Document;
const granularities = [
'neighbourhood',
'borough',
'locality',
'localadmin',
'county',
'macrocounty',
'region',
'macroregion',
'dependency',
'country'
];
function getMostGranularLayer(results) {
return granularities.find((granularity) => {
return results.hasOwnProperty(granularity);
});
}
function hasResultsAtRequestedLayers(results, layers) {
return _.intersection(layers, Object.keys(results)).length > 0;
}
function synthesizeDoc(results) {
// now create a model.Document from what's level, using the most granular
// result available as the starting point
// the immediately above cannot be re-used since county may be the most
// granular layer requested but the results may start at region (no county found)
const most_granular_layer = getMostGranularLayer(results);
const id = results[most_granular_layer][0].id;
const doc = new Document('whosonfirst', most_granular_layer, id.toString());
doc.setName('default', results[most_granular_layer][0].name);
if (results[most_granular_layer][0].hasOwnProperty('centroid')) {
doc.setCentroid( results[most_granular_layer][0].centroid );
}
if (results[most_granular_layer][0].hasOwnProperty('bounding_box')) {
const parsedBoundingBox = results[most_granular_layer][0].bounding_box.split(',').map(parseFloat);
doc.setBoundingBox({
upperLeft: {
lat: parsedBoundingBox[3],
lon: parsedBoundingBox[0]
},
lowerRight: {
lat: parsedBoundingBox[1],
lon: parsedBoundingBox[2]
}
});
}
if (_.has(results, 'country[0].abbr')) {
doc.setAlpha3(results.country[0].abbr);
}
// assign the administrative hierarchy
Object.keys(results).forEach((layer) => {
if (results[layer][0].hasOwnProperty('abbr')) {
doc.addParent(layer, results[layer][0].name, results[layer][0].id.toString(), results[layer][0].abbr);
} else {
doc.addParent(layer, results[layer][0].name, results[layer][0].id.toString());
}
});
const esDoc = doc.toESDocument();
esDoc.data._id = esDoc._id;
esDoc.data._type = esDoc._type;
return esDoc.data;
}
function setup(service, should_execute) {
function controller(req, res, next) {
// do not run controller when a request validation error has occurred
if (!should_execute(req, res)) {
return next();
}
const centroid = {
lat: req.clean['point.lat'],
lon: req.clean['point.lon']
};
service(centroid, (err, results) => {
// if there's an error, log it and bail
if (err) {
logger.error(err);
return next();
}
// find the finest granularity requested
const finest_granularity_requested = granularities.findIndex((granularity) => {
return req.clean.layers.indexOf(granularity) !== -1;
});
// now remove everything from the response that is more granular than the
// most granular layer requested. that is, if req.clean.layers=['county'],
// remove neighbourhoods, localities, and localadmins
Object.keys(results).forEach((layer) => {
if (granularities.indexOf(layer) < finest_granularity_requested) {
delete results[layer];
}
});
res.meta = {};
res.data = [];
// synthesize a doc from results if there's a result at the request layer(s)
if (hasResultsAtRequestedLayers(results, req.clean.layers)) {
res.data.push(synthesizeDoc(results));
}
return next();
});
}
return controller;
}
module.exports = setup;

5
controller/predicates/has_data.js

@ -0,0 +1,5 @@
const _ = require('lodash');
module.exports = (request, response) => {
return _.get(response, 'data', []).length > 0;
};

5
controller/predicates/has_errors.js

@ -0,0 +1,5 @@
const _ = require('lodash');
module.exports = (request, response) => {
return _.get(request, 'errors', []).length > 0;
};

7
controller/predicates/is_coarse_reverse.js

@ -0,0 +1,7 @@
const _ = require('lodash');
module.exports = (req, res) => {
// returns true if layers is undefined, empty, or contains 'address' or 'venue'
return !_.isEmpty(req.clean.layers) &&
_.intersection(req.clean.layers, ['address', 'venue']).length === 0;
};

21
controller/search.js

@ -7,30 +7,13 @@ const logger = require('pelias-logger').get('api');
const logging = require( '../helper/logging' );
const retry = require('retry');
function requestHasErrors(request) {
return _.get(request, 'errors', []).length > 0;
}
function responseHasData(response) {
return _.get(response, 'data', []).length > 0;
}
function isRequestTimeout(err) {
return _.get(err, 'status') === 408;
}
function setup( apiConfig, esclient, query ){
function setup( apiConfig, esclient, query, should_execute ){
function controller( req, res, next ){
// do not run controller when a request
// validation error has occurred.
if (requestHasErrors(req)) {
return next();
}
// do not run controller if there are already results
// this was added during libpostal integration. if the libpostal parse/query
// doesn't return anything then fallback to old search-engine-y behavior
if (responseHasData(res)) {
if (!should_execute(req, res)) {
return next();
}

3
package.json

@ -56,9 +56,11 @@
"pelias-config": "2.8.0",
"pelias-labels": "1.5.1",
"pelias-logger": "0.1.0",
"pelias-mock-logger": "^1.0.1",
"pelias-model": "4.5.1",
"pelias-query": "8.13.0",
"pelias-text-analyzer": "1.7.2",
"predicates": "^1.0.1",
"retry": "^0.10.1",
"stats-lite": "^2.0.4",
"superagent": "^3.2.1",
@ -72,6 +74,7 @@
"nsp": "^2.2.0",
"precommit-hook": "^3.0.0",
"proxyquire": "^1.7.10",
"request": "^2.79.0",
"semantic-release": "^6.3.2",
"source-map": "^0.5.6",
"tap-dot": "1.0.5",

29
routes/v1.js

@ -1,6 +1,10 @@
var Router = require('express').Router;
var elasticsearch = require('elasticsearch');
const all = require('predicates').all;
const any = require('predicates').any;
const not = require('predicates').not;
/** ----------------------- sanitizers ----------------------- **/
var sanitizers = {
autocomplete: require('../sanitizer/autocomplete'),
@ -20,6 +24,7 @@ var middleware = {
/** ----------------------- controllers ----------------------- **/
var controllers = {
coarse_reverse: require('../controller/coarse_reverse'),
mdToHTML: require('../controller/markdownToHtml'),
place: require('../controller/place'),
search: require('../controller/search'),
@ -55,6 +60,17 @@ var postProc = {
assignLabels: require('../middleware/assignLabels')
};
// make configurable to return do-nothing service if PIP URL not set
const pipService = require('../service/pointinpolygon')('http://localhost:3102');
// predicates that drive whether controller/search runs
const hasData = require('../controller/predicates/has_data');
const hasErrors = require('../controller/predicates/has_errors');
const isCoarseReverse = require('../controller/predicates/is_coarse_reverse');
// shorthand for standard early-exit conditions
const hasDataOrErrors = any(hasData, hasErrors);
/**
* Append routes to app
*
@ -80,9 +96,9 @@ function addRoutes(app, peliasConfig) {
middleware.calcSize(),
// 3rd parameter is which query module to use, use fallback/geodisambiguation
// first, then use original search strategy if first query didn't return anything
controllers.search(peliasConfig.api, esclient, queries.libpostal),
controllers.search(peliasConfig.api, esclient, queries.libpostal, not(hasDataOrErrors)),
sanitizers.search_fallback.middleware,
controllers.search(peliasConfig.api, esclient, queries.fallback_to_old_prod),
controllers.search(peliasConfig.api, esclient, queries.fallback_to_old_prod, not(hasDataOrErrors)),
postProc.trimByGranularity(),
postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig.api),
@ -101,7 +117,7 @@ function addRoutes(app, peliasConfig) {
structured: createRouter([
sanitizers.structured_geocoding.middleware,
middleware.calcSize(),
controllers.search(peliasConfig.api, esclient, queries.structured_geocoding),
controllers.search(peliasConfig.api, esclient, queries.structured_geocoding, not(hasDataOrErrors)),
postProc.trimByGranularityStructured(),
postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig.api),
@ -119,7 +135,7 @@ function addRoutes(app, peliasConfig) {
]),
autocomplete: createRouter([
sanitizers.autocomplete.middleware,
controllers.search(peliasConfig.api, esclient, queries.autocomplete),
controllers.search(peliasConfig.api, esclient, queries.autocomplete, not(hasDataOrErrors)),
postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig.api),
postProc.dedupe(),
@ -135,7 +151,8 @@ function addRoutes(app, peliasConfig) {
reverse: createRouter([
sanitizers.reverse.middleware,
middleware.calcSize(),
controllers.search(peliasConfig.api, esclient, queries.reverse),
controllers.coarse_reverse(pipService, all(isCoarseReverse, not(hasErrors))),
controllers.search(peliasConfig.api, esclient, queries.reverse, not(any(hasDataOrErrors, isCoarseReverse))),
postProc.distances('point.'),
// reverse confidence scoring depends on distance from origin
// so it must be calculated first
@ -153,7 +170,7 @@ function addRoutes(app, peliasConfig) {
nearby: createRouter([
sanitizers.nearby.middleware,
middleware.calcSize(),
controllers.search(peliasConfig.api, esclient, queries.reverse),
controllers.search(peliasConfig.api, esclient, queries.reverse, not(hasDataOrErrors)),
postProc.distances('point.'),
// reverse confidence scoring depends on distance from origin
// so it must be calculated first

33
service/pointinpolygon.js

@ -0,0 +1,33 @@
const logger = require( 'pelias-logger' ).get( 'pointinpolygon' );
const request = require('request');
module.exports = (url) => {
function service( centroid, callback ){
const requestUrl = `${url}/${centroid.lon}/${centroid.lat}`;
request.get(requestUrl, (err, response, body) => {
if (err) {
logger.error(JSON.stringify(err));
callback(err);
}
else if (response.statusCode === 200) {
try {
const parsed = JSON.parse(body);
callback(err, parsed);
}
catch (err) {
logger.error(`${requestUrl}: could not parse response body: ${body}`);
callback(`${requestUrl} returned status 200 but with non-JSON response: ${body}`);
}
}
else {
logger.error(`${requestUrl} returned status ${response.statusCode}: ${body}`);
callback(`${requestUrl} returned status ${response.statusCode}: ${body}`);
}
});
}
return service;
};

578
test/unit/controller/coarse_reverse.js

@ -0,0 +1,578 @@
'use strict';
const setup = require('../../../controller/coarse_reverse');
const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end();
});
};
module.exports.tests.early_exit_conditions = (test, common) => {
test('should_execute returning false should not call service', (t) => {
const service = () => {
throw Error('service should not have been called');
};
const should_execute = () => { return false; };
const controller = setup(service, should_execute);
const req = {
clean: {
layers: ['locality']
},
errors: ['error']
};
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
// passing res=undefined verifies that it wasn't interacted with
t.doesNotThrow(controller.bind(null, req, undefined, next));
t.ok(next_was_called);
t.end();
});
};
module.exports.tests.error_conditions = (test, common) => {
test('service error should log and call next', (t) => {
const service = (point, callback) => {
callback('this is an error');
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['locality'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
// passing res=undefined verifies that it wasn't interacted with
controller(req, undefined, next);
t.ok(logger.isErrorMessage('this is an error'));
t.ok(next_was_called);
t.end();
});
};
module.exports.tests.success_conditions = (test, common) => {
test('service returning results should use first entry for each layer', (t) => {
const service = (point, callback) => {
const results = {
neighbourhood: [
{
id: 10,
name: 'neighbourhood name',
abbr: 'neighbourhood abbr',
centroid: {
lat: 12.121212,
lon: 21.212121
},
bounding_box: '-76.345902,40.006751,-76.254038,40.072939'
},
{ id: 11, name: 'neighbourhood name 2'}
],
borough: [
{ id: 20, name: 'borough name', abbr: 'borough abbr'},
{ id: 21, name: 'borough name 2'}
],
locality: [
{ id: 30, name: 'locality name', abbr: 'locality abbr'},
{ id: 31, name: 'locality name 2'}
],
localadmin: [
{ id: 40, name: 'localadmin name', abbr: 'localadmin abbr'},
{ id: 41, name: 'localadmin name 2'}
],
county: [
{ id: 50, name: 'county name', abbr: 'county abbr'},
{ id: 51, name: 'county name 2'}
],
macrocounty: [
{ id: 60, name: 'macrocounty name', abbr: 'macrocounty abbr'},
{ id: 61, name: 'macrocounty name 2'}
],
region: [
{ id: 70, name: 'region name', abbr: 'region abbr'},
{ id: 71, name: 'region name 2'}
],
macroregion: [
{ id: 80, name: 'macroregion name', abbr: 'macroregion abbr'},
{ id: 81, name: 'macroregion name 2'}
],
dependency: [
{ id: 90, name: 'dependency name', abbr: 'dependency abbr'},
{ id: 91, name: 'dependency name 2'}
],
country: [
{ id: 100, name: 'country name', abbr: 'xyz'},
{ id: 101, name: 'country name 2'}
]
};
callback(undefined, results);
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['neighbourhood'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
const res = { };
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
controller(req, res, next);
const expected = {
meta: {},
data: [
{
_id: '10',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '10',
name: {
'default': 'neighbourhood name'
},
phrase: {
'default': 'neighbourhood name'
},
parent: {
neighbourhood: ['neighbourhood name'],
neighbourhood_id: ['10'],
neighbourhood_a: ['neighbourhood abbr'],
borough: ['borough name'],
borough_id: ['20'],
borough_a: ['borough abbr'],
locality: ['locality name'],
locality_id: ['30'],
locality_a: ['locality abbr'],
localadmin: ['localadmin name'],
localadmin_id: ['40'],
localadmin_a: ['localadmin abbr'],
county: ['county name'],
county_id: ['50'],
county_a: ['county abbr'],
macrocounty: ['macrocounty name'],
macrocounty_id: ['60'],
macrocounty_a: ['macrocounty abbr'],
region: ['region name'],
region_id: ['70'],
region_a: ['region abbr'],
macroregion: ['macroregion name'],
macroregion_id: ['80'],
macroregion_a: ['macroregion abbr'],
dependency: ['dependency name'],
dependency_id: ['90'],
dependency_a: ['dependency abbr'],
country: ['country name'],
country_id: ['100'],
country_a: ['xyz']
},
alpha3: 'XYZ',
center_point: {
lat: 12.121212,
lon: 21.212121
},
bounding_box: '{"min_lat":40.006751,"max_lat":40.072939,"min_lon":-76.345902,"max_lon":-76.254038}'
}
]
};
t.deepEquals(res, expected);
t.notOk(logger.hasErrorMessages());
t.ok(next_was_called);
t.end();
});
test('layers missing from results should be ignored', (t) => {
const service = (point, callback) => {
const results = {
neighbourhood: [
{
id: 10,
name: 'neighbourhood name',
abbr: 'neighbourhood abbr',
centroid: {
lat: 12.121212,
lon: 21.212121
},
bounding_box: '-76.345902,40.006751,-76.254038,40.072939'
}
]
};
callback(undefined, results);
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['neighbourhood'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
const res = { };
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
controller(req, res, next);
const expected = {
meta: {},
data: [
{
_id: '10',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '10',
name: {
'default': 'neighbourhood name'
},
phrase: {
'default': 'neighbourhood name'
},
parent: {
neighbourhood: ['neighbourhood name'],
neighbourhood_id: ['10'],
neighbourhood_a: ['neighbourhood abbr']
},
center_point: {
lat: 12.121212,
lon: 21.212121
},
bounding_box: '{"min_lat":40.006751,"max_lat":40.072939,"min_lon":-76.345902,"max_lon":-76.254038}'
}
]
};
t.deepEquals(res, expected);
t.notOk(logger.hasErrorMessages());
t.ok(next_was_called);
t.end();
});
test('most granular layer missing centroid should not set', (t) => {
const service = (point, callback) => {
const results = {
neighbourhood: [
{
id: 10,
name: 'neighbourhood name',
abbr: 'neighbourhood abbr',
bounding_box: '-76.345902,40.006751,-76.254038,40.072939'
}
]
};
callback(undefined, results);
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['neighbourhood'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
const res = { };
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
controller(req, res, next);
const expected = {
meta: {},
data: [
{
_id: '10',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '10',
name: {
'default': 'neighbourhood name'
},
phrase: {
'default': 'neighbourhood name'
},
parent: {
neighbourhood: ['neighbourhood name'],
neighbourhood_id: ['10'],
neighbourhood_a: ['neighbourhood abbr']
},
bounding_box: '{"min_lat":40.006751,"max_lat":40.072939,"min_lon":-76.345902,"max_lon":-76.254038}'
}
]
};
t.deepEquals(res, expected);
t.notOk(logger.hasErrorMessages());
t.ok(next_was_called);
t.end();
});
test('most granular layer missing bounding_box should not set', (t) => {
const service = (point, callback) => {
const results = {
neighbourhood: [
{
id: 10,
name: 'neighbourhood name',
abbr: 'neighbourhood abbr',
centroid: {
lat: 12.121212,
lon: 21.212121
}
}
]
};
callback(undefined, results);
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['neighbourhood'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
const res = { };
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
controller(req, res, next);
const expected = {
meta: {},
data: [
{
_id: '10',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '10',
name: {
'default': 'neighbourhood name'
},
phrase: {
'default': 'neighbourhood name'
},
parent: {
neighbourhood: ['neighbourhood name'],
neighbourhood_id: ['10'],
neighbourhood_a: ['neighbourhood abbr']
},
center_point: {
lat: 12.121212,
lon: 21.212121
}
}
]
};
t.deepEquals(res, expected);
t.notOk(logger.hasErrorMessages());
t.ok(next_was_called);
t.end();
});
};
module.exports.tests.failure_conditions = (test, common) => {
test('service returning 0 results at the requested layer should return nothing', (t) => {
const service = (point, callback) => {
// response without neighbourhood results
const results = {
borough: [
{ id: 20, name: 'borough name', abbr: 'borough abbr'},
{ id: 21, name: 'borough name 2'}
],
locality: [
{ id: 30, name: 'locality name', abbr: 'locality abbr'},
{ id: 31, name: 'locality name 2'}
],
localadmin: [
{ id: 40, name: 'localadmin name', abbr: 'localadmin abbr'},
{ id: 41, name: 'localadmin name 2'}
],
county: [
{ id: 50, name: 'county name', abbr: 'county abbr'},
{ id: 51, name: 'county name 2'}
],
macrocounty: [
{ id: 60, name: 'macrocounty name', abbr: 'macrocounty abbr'},
{ id: 61, name: 'macrocounty name 2'}
],
region: [
{ id: 70, name: 'region name', abbr: 'region abbr'},
{ id: 71, name: 'region name 2'}
],
macroregion: [
{ id: 80, name: 'macroregion name', abbr: 'macroregion abbr'},
{ id: 81, name: 'macroregion name 2'}
],
dependency: [
{ id: 90, name: 'dependency name', abbr: 'dependency abbr'},
{ id: 91, name: 'dependency name 2'}
],
country: [
{ id: 100, name: 'country name', abbr: 'xyz'},
{ id: 101, name: 'country name 2'}
]
};
callback(undefined, results);
};
const logger = require('pelias-mock-logger')();
const should_execute = () => { return true; };
const controller = proxyquire('../../../controller/coarse_reverse', {
'pelias-logger': logger
})(service, should_execute);
const req = {
clean: {
layers: ['neighbourhood'],
point: {
lat: 12.121212,
lon: 21.212121
}
}
};
const res = { };
// verify that next was called
let next_was_called = false;
const next = () => {
next_was_called = true;
};
controller(req, res, next);
const expected = {
meta: {},
data: []
};
t.deepEquals(res, expected);
t.notOk(logger.hasErrorMessages());
t.ok(next_was_called);
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /coarse_reverse ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

2
test/unit/controller/place.js

@ -1,6 +1,6 @@
'use strict';
const setup = require('../../../controller/search');
const setup = require('../../../controller/place');
const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {};

60
test/unit/controller/predicates/has_data.js

@ -0,0 +1,60 @@
'use strict';
const _ = require('lodash');
const has_data = require('../../../../controller/predicates/has_data');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.equal(typeof has_data, 'function', 'has_data is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('response with non-empty data should return true', (t) => {
const req = {};
const res = {
data: [1]
};
t.ok(has_data(req, res));
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('response with undefined data should return true', (t) => {
const req = {};
const res = {};
t.notOk(has_data(req, res));
t.end();
});
test('response with empty data array should return true', (t) => {
const req = {};
const res = {
data: []
};
t.notOk(has_data(req, res));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /has_data ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

60
test/unit/controller/predicates/has_errors.js

@ -0,0 +1,60 @@
'use strict';
const _ = require('lodash');
const has_errors = require('../../../../controller/predicates/has_errors');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.equal(typeof has_errors, 'function', 'has_errors is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('request with non-empty errors should return true', (t) => {
const req = {
errors: ['error']
};
const res = {};
t.ok(has_errors(req, res));
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('response with undefined errors should return false', (t) => {
const req = {};
const res = {};
t.notOk(has_errors(req, res));
t.end();
});
test('response with empty errors array should return false', (t) => {
const req = {
errors: []
};
const res = {};
t.notOk(has_errors(req, res));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /has_errors ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

128
test/unit/controller/predicates/is_coarse_reverse.js

@ -0,0 +1,128 @@
'use strict';
const _ = require('lodash');
const is_coarse_reverse = require('../../../../controller/predicates/is_coarse_reverse');
const coarse_layers = [
'continent',
'country',
'dependency',
'macroregion',
'region',
'locality',
'localadmin',
'macrocounty',
'county',
'macrohood',
'borough',
'neighbourhood',
'microhood',
'disputed'
];
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.equal(typeof is_coarse_reverse, 'function', 'is_coarse_reverse is a function');
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('request without layers should return false', (t) => {
const req = {
clean: {}
};
t.notOk(is_coarse_reverse(req));
t.end();
});
test('request with empty layers should return false', (t) => {
const req = {
clean: {
layers: []
}
};
t.notOk(is_coarse_reverse(req));
t.end();
});
test('request with layers just "address" or "venue" return false', (t) => {
['address', 'venue'].forEach((non_coarse_layer) => {
const req = {
clean: {
layers: [non_coarse_layer]
}
};
t.notOk(is_coarse_reverse(req));
});
t.end();
});
test('request with layers containing "address" or "venue" and a coarse layer should return false', (t) => {
['address', 'venue'].forEach((non_coarse_layer) => {
const req = {
clean: {
layers: [_.sample(coarse_layers), non_coarse_layer]
}
};
t.notOk(is_coarse_reverse(req));
});
t.end();
});
test('request with layers containing "address" and "venue" should return false', (t) => {
const req = {
clean: {
layers: ['address', 'venue']
}
};
t.notOk(is_coarse_reverse(req));
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('request with non-empty layers and not containing "address" or "venue" should return true', (t) => {
coarse_layers.forEach((coarse_layer) => {
const req = {
clean: {
layers: [coarse_layer]
}
};
t.ok(is_coarse_reverse(req));
});
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /is_coarse_reverse ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

58
test/unit/controller/search.js

@ -55,7 +55,7 @@ module.exports.tests.success = function(test, common) {
};
}
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -119,7 +119,7 @@ module.exports.tests.success = function(test, common) {
};
}
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -183,7 +183,7 @@ module.exports.tests.success = function(test, common) {
};
}
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -263,7 +263,7 @@ module.exports.tests.success = function(test, common) {
};
}
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -341,7 +341,7 @@ module.exports.tests.timeout = function(test, common) {
};
}
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -392,7 +392,7 @@ module.exports.tests.timeout = function(test, common) {
callback(timeoutError);
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -432,7 +432,7 @@ module.exports.tests.timeout = function(test, common) {
callback(nonTimeoutError);
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -473,7 +473,7 @@ module.exports.tests.timeout = function(test, common) {
callback(stringTypeError);
}
})(config, esclient, query);
})(config, esclient, query, () => { return true; });
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
@ -494,49 +494,21 @@ module.exports.tests.timeout = function(test, common) {
};
module.exports.tests.existing_errors = function(test, common) {
test('req with errors should not call esclient or query', function(t) {
module.exports.tests.should_execute = (test, common) => {
test('should_execute returning false and empty req.errors should call next', (t) => {
const esclient = () => {
throw new Error('esclient should not have been called');
};
const query = () => {
throw new Error('query should not have been called');
};
const controller = setup( {}, esclient, query );
// the existence of `errors` means that a sanitizer detected an error,
// so don't call the esclient
const req = {
errors: ['error']
};
const res = { };
t.doesNotThrow(() => {
controller(req, res, () => {});
});
t.end();
});
};
module.exports.tests.existing_results = function(test, common) {
test('res with existing data should not call esclient or query', function(t) {
const esclient = () => {
throw new Error('esclient should not have been called');
};
const query = () => {
throw new Error('query should not have been called');
};
const controller = setup( {}, esclient, query );
const controller = setup( {}, esclient, query, () => { return false; } );
const req = { };
// the existence of `data` means that there are already results so
// don't call esclient or query
const res = { data: [{}] };
const res = { };
const next = function() {
t.deepEqual(res, {data: [{}]});
const next = () => {
t.deepEqual(res, { });
t.end();
};
controller(req, res, next);
@ -559,7 +531,7 @@ module.exports.tests.undefined_query = function(test, common) {
search_service_was_called = true;
throw new Error('search service should not have been called');
}
})(undefined, undefined, query);
})(undefined, undefined, query, () => { return true; });
const next = () => {
t.notOk(search_service_was_called, 'should have returned before search service was called');

7
test/unit/run.js

@ -11,9 +11,13 @@ var common = {
var tests = [
require('./app'),
require('./schema'),
require('./controller/coarse_reverse'),
require('./controller/index'),
require('./controller/place'),
require('./controller/search'),
require('./controller/predicates/has_data'),
require('./controller/predicates/has_errors'),
require('./controller/predicates/is_coarse_reverse'),
require('./helper/diffPlaces'),
require('./helper/geojsonify'),
require('./helper/logging'),
@ -73,7 +77,8 @@ var tests = [
require('./sanitizer/wrap'),
require('./service/mget'),
require('./service/search'),
require('./service/interpolation')
require('./service/interpolation'),
require('./service/pointinpolygon')
];
tests.map(function(t) {

140
test/unit/service/pointinpolygon.js

@ -0,0 +1,140 @@
const proxyquire = require('proxyquire').noCallThru();
const setup = require('../../../service/pointinpolygon');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
const logger = require('pelias-mock-logger')();
var service = proxyquire('../../../service/pointinpolygon', {
'pelias-logger': logger
});
t.equal(typeof service, 'function', 'service is a function');
t.end();
});
};
module.exports.tests.success = (test, common) => {
test('lat and lon should be passed to server', (t) => {
const pipServer = require('express')();
pipServer.get('/:lon/:lat', (req, res, next) => {
t.equals(req.params.lat, '12.121212');
t.equals(req.params.lon, '21.212121');
res.send('{ "field": "value" }');
});
const server = pipServer.listen();
const service = setup(`http://localhost:${server.address().port}`);
service({ lat: 12.121212, lon: 21.212121}, (err, results) => {
t.notOk(err);
t.deepEquals(results, { field: 'value' });
t.end();
server.close();
});
});
};
module.exports.tests.failure = (test, common) => {
test('server returning success but non-JSON body should log error and return no results', (t) => {
const pipServer = require('express')();
pipServer.get('/:lon/:lat', (req, res, next) => {
t.equals(req.params.lat, '12.121212');
t.equals(req.params.lon, '21.212121');
res.send('this is not JSON');
});
const server = pipServer.listen();
const port = server.address().port;
const logger = require('pelias-mock-logger')();
const service = proxyquire('../../../service/pointinpolygon', {
'pelias-logger': logger
})(`http://localhost:${port}`);
service({ lat: 12.121212, lon: 21.212121}, (err, results) => {
t.equals(err, `http://localhost:${port}/21.212121/12.121212 returned status 200 but with non-JSON response: this is not JSON`);
t.notOk(results);
t.ok(logger.isErrorMessage(`http://localhost:${port}/21.212121/12.121212: could not parse response body: this is not JSON`));
t.end();
server.close();
});
});
test('server returning error should log it and return no results', (t) => {
const server = require('express')().listen();
const port = server.address().port;
// immediately close the server so to ensure an error response
server.close();
const logger = require('pelias-mock-logger')();
const service = proxyquire('../../../service/pointinpolygon', {
'pelias-logger': logger
})(`http://localhost:${port}`);
service({ lat: 12.121212, lon: 21.212121}, (err, results) => {
t.equals(err.code, 'ECONNREFUSED');
t.notOk(results);
t.ok(logger.isErrorMessage(/ECONNREFUSED/), 'there should be a connection refused error message');
t.end();
server.close();
});
});
test('non-OK status should log error and return no results', (t) => {
const pipServer = require('express')();
pipServer.get('/:lat/:lon', (req, res, next) => {
res.status(400).send('a bad request was made');
});
const server = pipServer.listen();
const port = server.address().port;
const logger = require('pelias-mock-logger')();
const service = proxyquire('../../../service/pointinpolygon', {
'pelias-logger': logger
})(`http://localhost:${port}`);
service({ lat: 12.121212, lon: 21.212121}, (err, results) => {
t.equals(err, `http://localhost:${port}/21.212121/12.121212 returned status 400: a bad request was made`);
t.notOk(results);
t.ok(logger.isErrorMessage(`http://localhost:${port}/21.212121/12.121212 returned status 400: a bad request was made`));
t.end();
server.close();
});
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`SERVICE /pointinpolygon ${name}`, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};
Loading…
Cancel
Save