Browse Source

Merge pull request #912 from pelias/addresses-placeholder-integration

Addresses placeholder integration
pull/944/head
Stephen K Hess 8 years ago committed by GitHub
parent
commit
04fad263bc
  1. 31
      controller/libpostal.js
  2. 62
      controller/placeholder.js
  3. 18
      controller/predicates/has_any_parsed_text_property.js
  4. 4
      controller/predicates/has_request_parameter.js
  5. 4
      controller/predicates/is_addressit_parse.js
  6. 2
      controller/predicates/is_admin_only_analysis.js
  7. 7
      controller/predicates/is_only_non_admin_layers.js
  8. 9
      controller/predicates/is_request_sources_only_whosonfirst.js
  9. 113
      controller/search_with_ids.js
  10. 4
      package.json
  11. 192
      query/address_search_using_ids.js
  12. 51
      query/search.js
  13. 120
      routes/v1.js
  14. 17
      sanitizer/_text.js
  15. 1
      sanitizer/_text_addressit.js
  16. 32
      sanitizer/defer_to_addressit.js
  17. 2
      sanitizer/search.js
  18. 30
      sanitizer/search_fallback.js
  19. 14
      service/configurations/PlaceHolder.js
  20. 290
      test/unit/controller/libpostal.js
  21. 619
      test/unit/controller/placeholder.js
  22. 94
      test/unit/controller/predicates/has_any_parsed_text_property.js
  23. 63
      test/unit/controller/predicates/has_request_parameter.js
  24. 73
      test/unit/controller/predicates/is_addressit_parse.js
  25. 4
      test/unit/controller/predicates/is_admin_only_analysis.js
  26. 111
      test/unit/controller/predicates/is_only_non_admin_layers.js
  27. 107
      test/unit/controller/predicates/is_request_sources_only_whosonfirst.js
  28. 569
      test/unit/controller/search_with_ids.js
  29. 27
      test/unit/query/MockQuery.js
  30. 553
      test/unit/query/address_search_using_ids.js
  31. 7
      test/unit/query/search.js
  32. 10
      test/unit/run.js
  33. 146
      test/unit/sanitizer/_text.js
  34. 13
      test/unit/sanitizer/_text_addressit.js
  35. 132
      test/unit/sanitizer/defer_to_addressit.js
  36. 10
      test/unit/sanitizer/search.js
  37. 185
      test/unit/sanitizer/search_fallback.js
  38. 53
      test/unit/service/configurations/PlaceHolder.js

31
controller/libpostal.js

@ -0,0 +1,31 @@
const text_analyzer = require('pelias-text-analyzer');
const _ = require('lodash');
const iso3166 = require('iso3166-1');
function setup(should_execute) {
function controller( req, res, next ){
// bail early if req/res don't pass conditions for execution
if (!should_execute(req, res)) {
return next();
}
// parse text with query parser
const parsed_text = text_analyzer.parse(req.clean.text);
if (parsed_text !== undefined) {
// if a known ISO2 country was parsed, convert it to ISO3
if (_.has(parsed_text, 'country') && iso3166.is2(_.toUpper(parsed_text.country))) {
parsed_text.country = iso3166.to3(_.toUpper(parsed_text.country));
}
req.clean.parsed_text = parsed_text;
}
return next();
}
return controller;
}
module.exports = setup;

62
controller/placeholder.js

@ -59,16 +59,16 @@ function hasName(result) {
}
// filter that passes only results that match on requested layers
// passes everything if req.clean.layers is not found
function getLayersFilter(clean) {
if (_.isEmpty(_.get(clean, 'layers', []))) {
return _.constant(true);
// passes everything if:
// - req.clean.layers is empty
// - req.clean.parsed_text.street is available
if (_.isEmpty(_.get(clean, 'layers', [])) || _.has(clean, ['parsed_text', 'street'])) {
return () => true;
}
// otherwise return a function that checks for set inclusion of a result placetype
return (result) => {
return _.includes(clean.layers, result.placetype);
};
return (result) => _.includes(clean.layers, result.placetype);
}
@ -85,20 +85,20 @@ function atLeastOneLineageMatchesBoundaryCountry(boundaryCountry, result) {
}
// return a function that detects if a result has at least one lineage in boundary.country
// if there's no boundary.country, return a function that always returns true
function getBoundaryCountryFilter(clean) {
if (_.has(clean, 'boundary.country')) {
function getBoundaryCountryFilter(clean, do_geometric_filters_apply) {
if ( do_geometric_filters_apply && _.has(clean, 'boundary.country') ) {
return _.partial(atLeastOneLineageMatchesBoundaryCountry, clean['boundary.country']);
}
return _.constant(true);
// there's no boundary.country filter, so return a function that always returns true
return () => true;
}
// return a function that detects if a result is inside a bbox if a bbox is available
// if there's no bbox, return a function that always returns true
function getBoundaryRectangleFilter(clean) {
if (['min_lat', 'min_lon', 'max_lat', 'max_lon'].every((f) => {
function getBoundaryRectangleFilter(clean, do_geometric_filters_apply) {
// check to see if boundary.rect.min_lat/min_lon/max_lat/max_lon are all available
if (do_geometric_filters_apply && ['min_lat', 'min_lon', 'max_lat', 'max_lon'].every((f) => {
return _.has(clean, `boundary.rect.${f}`);
})) {
const polygon = [
@ -107,20 +107,22 @@ function getBoundaryRectangleFilter(clean) {
{ latitude: clean['boundary.rect.max_lat'], longitude: clean['boundary.rect.max_lon'] },
{ latitude: clean['boundary.rect.min_lat'], longitude: clean['boundary.rect.max_lon'] }
];
// isPointInside takes polygon last, so create a function that has it pre-populated
const isPointInsidePolygon = _.partialRight(geolib.isPointInside, polygon);
return _.partial(isInsideGeometry, isPointInsidePolygon);
}
return _.constant(true);
// there's no bbox filter, so return a function that always returns true
return () => true;
}
// return a function that detects if a result is inside a circle if a circle is available
// if there's no circle, return a function that always returns true
function getBoundaryCircleFilter(clean) {
if (['lat', 'lon', 'radius'].every((f) => {
function getBoundaryCircleFilter(clean, do_geometric_filters_apply) {
// check to see if boundary.circle.lat/lon/radius are all available
if (do_geometric_filters_apply && ['lat', 'lon', 'radius'].every((f) => {
return _.has(clean, `boundary.circle.${f}`);
})) {
const center = {
@ -128,13 +130,16 @@ function getBoundaryCircleFilter(clean) {
longitude: clean['boundary.circle.lon']
};
const radiusInMeters = clean['boundary.circle.radius'] * 1000;
// isPointInCircle takes circle/radius last, so create a function that has them pre-populated
const isPointInCircle = _.partialRight(geolib.isPointInCircle, center, radiusInMeters);
return _.partial(isInsideGeometry, isPointInCircle);
}
return _.constant(true);
// there's no circle filter, so return a function that always returns true
return () => true;
}
@ -143,6 +148,7 @@ function isInsideGeometry(f, result) {
return hasLatLon(result) ? f(getLatLon(result)) : false;
}
// returns true if hierarchyElement has both name and id
function placetypeHasNameAndId(hierarchyElement) {
return !_.isEmpty(_.trim(hierarchyElement.name)) &&
!_.isEmpty(_.trim(hierarchyElement.id));
@ -160,7 +166,7 @@ function synthesizeDocs(boundaryCountry, result) {
logger.error(`could not parse centroid for id ${result.id}`);
}
// lodash conformsTo verifies that an object has a property with a certain format
// _.conformsTo verifies that an object property has a certain format
if (_.conformsTo(result.geom, { 'bbox': is4CommaDelimitedNumbers } )) {
const parsedBoundingBox = result.geom.bbox.split(',').map(_.toFinite);
doc.setBoundingBox({
@ -213,7 +219,7 @@ function buildESDoc(doc) {
return _.extend(esDoc.data, { _id: esDoc._id, _type: esDoc._type });
}
function setup(placeholderService, should_execute) {
function setup(placeholderService, do_geometric_filters_apply, should_execute) {
function controller( req, res, next ){
// bail early if req/res don't pass conditions for execution
if (!should_execute(req, res)) {
@ -222,15 +228,11 @@ function setup(placeholderService, should_execute) {
placeholderService(req, (err, results) => {
if (err) {
// bubble up an error if one occurred
if (_.isObject(err) && err.message) {
req.errors.push( err.message );
} else {
req.errors.push( err );
}
// push err.message or err onto req.errors
req.errors.push( _.get(err, 'message', err));
} else {
const boundaryCountry = _.get(req, ['clean', 'boundary.country']);
const boundaryCountry = do_geometric_filters_apply ? _.get(req, ['clean', 'boundary.country']) : undefined;
// convert results to ES docs
// boundary.country filter must happen after synthesis since multiple
@ -245,13 +247,13 @@ function setup(placeholderService, should_execute) {
// filter out results that don't match on requested layer(s)
.filter(getLayersFilter(req.clean))
// filter out results that don't match on any lineage country
.filter(getBoundaryCountryFilter(req.clean))
.filter(getBoundaryCountryFilter(req.clean, do_geometric_filters_apply))
// clean up geom.lat/lon for boundary rect/circle checks
.map(numberifyGeomLatLon)
// filter out results that aren't in the boundary.rect
.filter(getBoundaryRectangleFilter(req.clean))
.filter(getBoundaryRectangleFilter(req.clean, do_geometric_filters_apply))
// filter out results that aren't in the boundary.circle
.filter(getBoundaryCircleFilter(req.clean))
.filter(getBoundaryCircleFilter(req.clean, do_geometric_filters_apply))
// convert results to ES docs
.map(_.partial(synthesizeDocs, boundaryCountry));

18
controller/predicates/has_any_parsed_text_property.js

@ -0,0 +1,18 @@
const _ = require('lodash');
// return true if any setup parameter is a key of request.clean.parsed_text
// "arguments" is only available in long-form function declarations, cannot be shortened to fat arrow syntax
// potential improvement: inject set operator to allow for any/all functionality
module.exports = function() {
// save off requested properties since arguments can't be referenced later
const properties = _.values(arguments);
// return true if any of the supplied properties are in clean.parsed_text
return (request, response) => !_.isEmpty(
_.intersection(
properties,
_.keys(_.get(request, ['clean', 'parsed_text'], {}))
)
);
};

4
controller/predicates/has_request_parameter.js

@ -0,0 +1,4 @@
const _ = require('lodash');
// returns true IFF req.clean has a key with the supplied name
module.exports = (parameter) => (req, res) => (_.has(req, ['clean', parameter]));

4
controller/predicates/is_addressit_parse.js

@ -0,0 +1,4 @@
const _ = require('lodash');
// returns true IFF req.clean.parser is addressit
module.exports = (req, res) => (_.get(req, 'clean.parser') === 'addressit');

2
controller/predicates/is_admin_only_analysis.js

@ -6,7 +6,7 @@ module.exports = (request, response) => {
}
// return true only if all non-admin properties of parsed_text are empty
return ['number', 'street', 'query', 'category'].every((prop) => {
return ['number', 'street', 'query', 'category', 'postalcode'].every((prop) => {
return _.isEmpty(request.clean.parsed_text[prop]);
});

7
controller/predicates/is_only_non_admin_layers.js

@ -0,0 +1,7 @@
const _ = require('lodash');
// return true IFF req.clean.layers is empty OR there are non-venue/address/street layers
module.exports = (req, res) => (
!_.isEmpty(_.get(req, 'clean.layers', [])) &&
_.isEmpty(_.difference(req.clean.layers, ['venue', 'address', 'street']))
);

9
controller/predicates/is_request_sources_only_whosonfirst.js

@ -0,0 +1,9 @@
const _ = require('lodash');
// returns true IFF 'whosonfirst' is the only requested source
module.exports = (req, res) => (
_.isEqual(
_.get(req, 'clean.sources', []),
['whosonfirst']
)
);

113
controller/search_with_ids.js

@ -0,0 +1,113 @@
const _ = require('lodash');
const searchService = require('../service/search');
const logger = require('pelias-logger').get('api');
const logging = require( '../helper/logging' );
const retry = require('retry');
function isRequestTimeout(err) {
return _.get(err, 'status') === 408;
}
function setup( apiConfig, esclient, query, should_execute ){
function controller( req, res, next ){
if (!should_execute(req, res)) {
return next();
}
const cleanOutput = _.cloneDeep(req.clean);
if (logging.isDNT(req)) {
logging.removeFields(cleanOutput);
}
// log clean parameters for stats
logger.info('[req]', `endpoint=${req.path}`, cleanOutput);
const renderedQuery = query(req.clean, res);
// if there's no query to call ES with, skip the service
if (_.isUndefined(renderedQuery)) {
return next();
}
// options for retry
// maxRetries is from the API config with default of 3
// factor of 1 means that each retry attempt will esclient requestTimeout
const operationOptions = {
retries: _.get(apiConfig, 'requestRetries', 3),
factor: 1,
minTimeout: _.get(esclient, 'transport.requestTimeout')
};
// setup a new operation
const operation = retry.operation(operationOptions);
// elasticsearch command
const cmd = {
index: apiConfig.indexName,
searchType: 'dfs_query_then_fetch',
body: renderedQuery.body
};
logger.debug( '[ES req]', cmd );
operation.attempt((currentAttempt) => {
// query elasticsearch
searchService( esclient, cmd, function( err, docs, meta ){
// returns true if the operation should be attempted again
// (handles bookkeeping of maxRetries)
// only consider for status 408 (request timeout)
if (isRequestTimeout(err) && operation.retry(err)) {
logger.info(`request timed out on attempt ${currentAttempt}, retrying`);
return;
}
// if execution has gotten this far then one of three things happened:
// - the request didn't time out
// - maxRetries has been hit so we're giving up
// - another error occurred
// in either case, handle the error or results
// error handler
if( err ){
// push err.message or err onto req.errors
req.errors.push( _.get(err, 'message', err));
}
else {
// log that a retry was successful
// most requests succeed on first attempt so this declutters log files
if (currentAttempt > 1) {
logger.info(`succeeded on retry ${currentAttempt-1}`);
}
// because this is used in response to placeholder, there may already
// be results. if there are no results from this ES call, don't overwrite
// what's already there from placeholder.
if (!_.isEmpty(docs)) {
res.data = docs;
res.meta = meta || {};
// store the query_type for subsequent middleware
res.meta.query_type = renderedQuery.type;
const messageParts = [
'[controller:search]',
`[queryType:${renderedQuery.type}]`,
`[es_result_count:${docs.length}]`
];
logger.info(messageParts.join(' '));
}
}
logger.debug('[ES response]', docs);
next();
});
});
}
return controller;
}
module.exports = setup;

4
package.json

@ -60,9 +60,9 @@
"pelias-logger": "0.2.0",
"pelias-microservice-wrapper": "1.1.3",
"pelias-model": "5.0.0",
"pelias-query": "8.16.1",
"pelias-query": "9.0.0",
"pelias-sorting": "1.0.1",
"pelias-text-analyzer": "1.8.3",
"pelias-text-analyzer": "1.9.0",
"predicates": "^1.0.1",
"retry": "^0.10.1",
"stats-lite": "^2.0.4",

192
query/address_search_using_ids.js

@ -0,0 +1,192 @@
const peliasQuery = require('pelias-query');
const defaults = require('./search_defaults');
const logger = require('pelias-logger').get('api');
const _ = require('lodash');
const check = require('check-types');
//------------------------------
// general-purpose search query
//------------------------------
const addressUsingIdsQuery = new peliasQuery.layout.AddressesUsingIdsQuery();
// scoring boost
addressUsingIdsQuery.score( peliasQuery.view.focus_only_function( peliasQuery.view.phrase ) );
// --------------------------------
// non-scoring hard filters
addressUsingIdsQuery.filter( peliasQuery.view.boundary_country );
addressUsingIdsQuery.filter( peliasQuery.view.boundary_circle );
addressUsingIdsQuery.filter( peliasQuery.view.boundary_rect );
addressUsingIdsQuery.filter( peliasQuery.view.sources );
// --------------------------------
// This query is a departure from traditional Pelias queries where textual
// names of admin areas were looked up. This query uses the ids returned by
// placeholder for lookups which dramatically reduces the amount of information
// that ES has to store and allows us to have placeholder handle altnames on
// behalf of Pelias.
//
// For the happy path, an input like '30 West 26th Street, Manhattan' would result
// in:
// neighbourhood_id in []
// borough_id in [421205771]
// locality_id in [85945171, 85940551, 85972655]
// localadmin_id in [404502889, 404499147, 404502891, 85972655]
//
// Where the ids are for all the various Manhattans. Each of those could
// conceivably be the Manhattan that the user was referring to so so all must be
// queried for at the same time.
//
// A counter example for this is '1 West Market Street, York, PA' where York, PA
// can be interpreted as a locality OR county. From experience, when there's
// ambiguity between locality and county for an input, the user is, with complete
// metaphysical certitude, referring to the city. If they were referring to the
// county, they would have entered 'York County, PA'. The point is that it's
// insufficient to just query for all ids because, in this case, '1 West Market Street'
// in other cities in York County, PA would be returned and would be both jarring
// to the user and almost certainly leads to incorrect results. For example,
// the following could be returned (all are towns in York County, PA):
// - 1 West Market Street, Dallastown, PA
// - 1 West Market Street, Fawn Grove, PA
// - 1 West Market Street, Shrewsbury, PA
// etc.
//
// To avoid this calamitous response, this query takes the approach of
// "granularity bands". That is, if there are any ids in the first set of any
// of these granularities:
// - neighbourhood
// - borough
// - locality
// - localadmin
// - region
// - macroregion
// - dependency
// - country
//
// then query for all ids in only those layers. Falling back, if there are
// no ids in those layers, query for the county/macrocounty layers.
//
// This methodology ensures that no happened-to-match-on-county results are returned.
//
// The decision was made to include all other layers in one to solve the issue
// where a country and city share a name, such as Mexico, which could be
// interpreted as a country AND city (in Missouri). The data itself will sort
// out which is correct. That is, it's unlikely that "11 Rock Springs Dr" exists
// in Mexico the country due to naming conventions and would be filtered out
// (though it could, but that's good because it's legitimate)
const granularity_bands = [
['neighbourhood', 'borough', 'locality', 'localadmin', 'region', 'macroregion', 'dependency', 'country'],
['county', 'macrocounty']
];
// returns IFF there are *any* results in the granularity band
function anyResultsAtGranularityBand(results, band) {
return results.some(result => _.includes(band, result.layer));
}
// returns the ids of results at the requested layer
function getIdsAtLayer(results, layer) {
return results.filter(result => result.layer === layer).map(_.property('source_id'));
}
/**
map request variables to query variables for all inputs
provided by this HTTP request. This function operates on res.data which is the
Document-ified placeholder repsonse.
**/
function generateQuery( clean, res ){
const vs = new peliasQuery.Vars( defaults );
const results = _.defaultTo(res.data, []);
const logParts = ['query:address_search_using_ids', 'parser:libpostal'];
// sources
if( !_.isEmpty(clean.sources) ) {
vs.var( 'sources', clean.sources);
logParts.push('param:sources');
}
// size
if( clean.querySize ) {
vs.var( 'size', clean.querySize );
logParts.push('param:querySize');
}
if( ! _.isEmpty(clean.parsed_text.number) ){
vs.var( 'input:housenumber', clean.parsed_text.number );
}
vs.var( 'input:street', clean.parsed_text.street );
// find the first granularity band for which there are results
const granularity_band = granularity_bands.find(band => anyResultsAtGranularityBand(results, band));
// if there's a granularity band, accumulate the ids from each layer in the band
// into an object mapping layer->ids of those layers
if (granularity_band) {
const layers_to_ids = granularity_band.reduce((acc, layer) => {
acc[layer] = getIdsAtLayer(res.data, layer);
return acc;
}, {});
// use an object here instead of calling `set` since that flattens out an
// object into key/value pairs and makes identifying layers harder in query module
vs.var('input:layers', layers_to_ids);
}
// focus point
if( check.number(clean['focus.point.lat']) &&
check.number(clean['focus.point.lon']) ){
vs.set({
'focus:point:lat': clean['focus.point.lat'],
'focus:point:lon': clean['focus.point.lon']
});
}
// boundary rect
if( check.number(clean['boundary.rect.min_lat']) &&
check.number(clean['boundary.rect.max_lat']) &&
check.number(clean['boundary.rect.min_lon']) &&
check.number(clean['boundary.rect.max_lon']) ){
vs.set({
'boundary:rect:top': clean['boundary.rect.max_lat'],
'boundary:rect:right': clean['boundary.rect.max_lon'],
'boundary:rect:bottom': clean['boundary.rect.min_lat'],
'boundary:rect:left': clean['boundary.rect.min_lon']
});
}
// boundary circle
if( check.number(clean['boundary.circle.lat']) &&
check.number(clean['boundary.circle.lon']) ){
vs.set({
'boundary:circle:lat': clean['boundary.circle.lat'],
'boundary:circle:lon': clean['boundary.circle.lon']
});
if( check.number(clean['boundary.circle.radius']) ){
vs.set({
'boundary:circle:radius': Math.round( clean['boundary.circle.radius'] ) + 'km'
});
}
}
// boundary country
if( check.string(clean['boundary.country']) ){
vs.set({
'boundary:country': clean['boundary.country']
});
}
// format the log parts into a single coherent string
logger.info(logParts.map(part => `[${part}]`).join(' '));
return {
type: 'fallback',
body: addressUsingIdsQuery.render(vs)
};
}
module.exports = generateQuery;

51
query/search.js

@ -10,16 +10,11 @@ const logger = require('pelias-logger').get('api');
// general-purpose search query
//------------------------------
var fallbackQuery = new peliasQuery.layout.FallbackQuery();
var geodisambiguationQuery = new peliasQuery.layout.GeodisambiguationQuery();
// scoring boost
fallbackQuery.score( peliasQuery.view.focus_only_function( peliasQuery.view.phrase ) );
fallbackQuery.score( peliasQuery.view.popularity_only_function );
fallbackQuery.score( peliasQuery.view.population_only_function );
geodisambiguationQuery.score( peliasQuery.view.focus_only_function( peliasQuery.view.phrase ) );
geodisambiguationQuery.score( peliasQuery.view.popularity_only_function );
geodisambiguationQuery.score( peliasQuery.view.population_only_function );
// --------------------------------
// non-scoring hard filters
@ -29,13 +24,6 @@ fallbackQuery.filter( peliasQuery.view.boundary_rect );
fallbackQuery.filter( peliasQuery.view.sources );
fallbackQuery.filter( peliasQuery.view.layers );
fallbackQuery.filter( peliasQuery.view.categories );
geodisambiguationQuery.filter( peliasQuery.view.boundary_country );
geodisambiguationQuery.filter( peliasQuery.view.boundary_circle );
geodisambiguationQuery.filter( peliasQuery.view.boundary_rect );
geodisambiguationQuery.filter( peliasQuery.view.sources );
geodisambiguationQuery.filter( peliasQuery.view.layers );
geodisambiguationQuery.filter( peliasQuery.view.categories );
// --------------------------------
/**
@ -147,10 +135,7 @@ function getQuery(vs) {
logger.info(`[query:search] [search_input_type:${determineQueryType(vs)}]`);
if (hasStreet(vs) ||
isCityStateOnlyWithOptionalCountry(vs) ||
isCityCountryOnly(vs) ||
isPostalCodeOnly(vs)) {
if (hasStreet(vs) || isPostalCodeOnly(vs)) {
return {
type: 'fallback',
body: fallbackQuery.render(vs)
@ -174,7 +159,8 @@ function determineQueryType(vs) {
return 'venue';
}
else if (['neighbourhood', 'borough', 'postcode', 'county', 'region','country'].some(
(layer)=> { return vs.isset(`input:${layer}`);})) {
layer => vs.isset(`input:${layer}`)
)) {
return 'admin';
}
return 'other';
@ -184,37 +170,8 @@ function hasStreet(vs) {
return vs.isset('input:street');
}
function isCityStateOnlyWithOptionalCountry(vs) {
var isSet = (layer) => {
return vs.isset(`input:${layer}`);
};
var allowedFields = ['locality', 'region'];
var disallowedFields = ['query', 'category', 'housenumber', 'street',
'neighbourhood', 'borough', 'postcode', 'county'];
return allowedFields.every(isSet) && !disallowedFields.some(isSet);
}
function isCityCountryOnly(vs) {
var isSet = (layer) => {
return vs.isset(`input:${layer}`);
};
var allowedFields = ['locality', 'country'];
var disallowedFields = ['query', 'category', 'housenumber', 'street',
'neighbourhood', 'borough', 'postcode', 'county', 'region'];
return allowedFields.every(isSet) &&
!disallowedFields.some(isSet);
}
function isPostalCodeOnly(vs) {
var isSet = (layer) => {
return vs.isset(`input:${layer}`);
};
var isSet = layer => vs.isset(`input:${layer}`);
var allowedFields = ['postcode'];
var disallowedFields = ['query', 'category', 'housenumber', 'street',

120
routes/v1.js

@ -11,7 +11,7 @@ var sanitizers = {
autocomplete: require('../sanitizer/autocomplete'),
place: require('../sanitizer/place'),
search: require('../sanitizer/search'),
search_fallback: require('../sanitizer/search_fallback'),
defer_to_addressit: require('../sanitizer/defer_to_addressit'),
structured_geocoding: require('../sanitizer/structured_geocoding'),
reverse: require('../sanitizer/reverse'),
nearby: require('../sanitizer/nearby')
@ -28,18 +28,21 @@ var middleware = {
var controllers = {
coarse_reverse: require('../controller/coarse_reverse'),
mdToHTML: require('../controller/markdownToHtml'),
libpostal: require('../controller/libpostal'),
place: require('../controller/place'),
placeholder: require('../controller/placeholder'),
search: require('../controller/search'),
search_with_ids: require('../controller/search_with_ids'),
status: require('../controller/status')
};
var queries = {
libpostal: require('../query/search'),
fallback_to_old_prod: require('../query/search_original'),
cascading_fallback: require('../query/search'),
very_old_prod: require('../query/search_original'),
structured_geocoding: require('../query/structured_geocoding'),
reverse: require('../query/reverse'),
autocomplete: require('../query/autocomplete')
autocomplete: require('../query/autocomplete'),
address_using_ids: require('../query/address_search_using_ids')
};
/** ----------------------- controllers ----------------------- **/
@ -66,16 +69,27 @@ var postProc = {
};
// predicates that drive whether controller/search runs
const hasAnyParsedTextProperty = require('../controller/predicates/has_any_parsed_text_property');
const hasResponseData = require('../controller/predicates/has_response_data');
const hasRequestErrors = require('../controller/predicates/has_request_errors');
const isCoarseReverse = require('../controller/predicates/is_coarse_reverse');
const isAdminOnlyAnalysis = require('../controller/predicates/is_admin_only_analysis');
const hasResultsAtLayers = require('../controller/predicates/has_results_at_layers');
const isAddressItParse = require('../controller/predicates/is_addressit_parse');
const hasRequestCategories = require('../controller/predicates/has_request_parameter')('categories');
const isOnlyNonAdminLayers = require('../controller/predicates/is_only_non_admin_layers');
// this can probably be more generalized
const isRequestSourcesOnlyWhosOnFirst = require('../controller/predicates/is_request_sources_only_whosonfirst');
// shorthand for standard early-exit conditions
const hasResponseDataOrRequestErrors = any(hasResponseData, hasRequestErrors);
const hasAdminOnlyResults = not(hasResultsAtLayers(['venue', 'address', 'street']));
const hasNumberButNotStreet = all(
hasAnyParsedTextProperty('number'),
not(hasAnyParsedTextProperty('street'))
);
const serviceWrapper = require('pelias-microservice-wrapper').service;
const PlaceHolder = require('../service/configurations/PlaceHolder');
const PointInPolygon = require('../service/configurations/PointInPolygon');
@ -102,8 +116,83 @@ function addRoutes(app, peliasConfig) {
isPipServiceEnabled, not(hasRequestErrors), not(hasResponseData)
);
const placeholderShouldExecute = all(
not(hasResponseDataOrRequestErrors), isPlaceholderServiceEnabled, isAdminOnlyAnalysis
const libpostalShouldExecute = all(
not(hasRequestErrors),
not(isRequestSourcesOnlyWhosOnFirst)
);
// execute placeholder if libpostal only parsed as admin-only and needs to
// be geodisambiguated
const placeholderGeodisambiguationShouldExecute = all(
not(hasResponseDataOrRequestErrors),
isPlaceholderServiceEnabled,
// check request.clean for several conditions first
not(
any(
// layers only contains venue, address, or street
isOnlyNonAdminLayers,
// don't geodisambiguate if categories were requested
hasRequestCategories
)
),
any(
// only geodisambiguate if libpostal returned only admin areas or libpostal was skipped
isAdminOnlyAnalysis,
isRequestSourcesOnlyWhosOnFirst
)
);
// execute placeholder if libpostal identified address parts but ids need to
// be looked up for admin parts
const placeholderIdsLookupShouldExecute = all(
not(hasResponseDataOrRequestErrors),
isPlaceholderServiceEnabled,
// check clean.parsed_text for several conditions that must all be true
all(
// run placeholder if clean.parsed_text has 'street'
hasAnyParsedTextProperty('street'),
// don't run placeholder if there's a query or category
not(hasAnyParsedTextProperty('query', 'category')),
// run placeholder if there are any adminareas identified
hasAnyParsedTextProperty('neighbourhood', 'borough', 'city', 'county', 'state', 'country')
)
);
const searchWithIdsShouldExecute = all(
not(hasRequestErrors),
// don't search-with-ids if there's a query or category
not(hasAnyParsedTextProperty('query', 'category')),
// there must be a street
hasAnyParsedTextProperty('street')
);
// placeholder should have executed, useful for determining whether to actually
// fallback or not (don't fallback to old search if the placeholder response
// should be honored as is)
const placeholderShouldHaveExecuted = any(
placeholderGeodisambiguationShouldExecute,
placeholderIdsLookupShouldExecute
);
// don't execute the cascading fallback query IF placeholder should have executed
// that way, if placeholder didn't return anything, don't try to find more things the old way
const fallbackQueryShouldExecute = all(
not(hasRequestErrors),
not(hasResponseData),
not(placeholderShouldHaveExecuted)
);
// defer to addressit for analysis IF there's no response AND placeholder should not have executed
const shouldDeferToAddressIt = all(
not(hasRequestErrors),
not(hasResponseData),
not(placeholderShouldHaveExecuted)
);
// call very old prod query if addressit was the parser
const oldProdQueryShouldExecute = all(
not(hasRequestErrors),
isAddressItParse
);
// execute under the following conditions:
@ -117,6 +206,10 @@ function addRoutes(app, peliasConfig) {
)
);
// helpers to replace vague booleans
const geometricFiltersApply = true;
const geometricFiltersDontApply = false;
var base = '/v1/';
/** ------------------------- routers ------------------------- **/
@ -132,12 +225,15 @@ function addRoutes(app, peliasConfig) {
sanitizers.search.middleware(peliasConfig.api),
middleware.requestLanguage,
middleware.calcSize(),
controllers.placeholder(placeholderService, placeholderShouldExecute),
// 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, not(hasResponseDataOrRequestErrors)),
sanitizers.search_fallback.middleware,
controllers.search(peliasConfig.api, esclient, queries.fallback_to_old_prod, not(hasResponseDataOrRequestErrors)),
controllers.libpostal(libpostalShouldExecute),
controllers.placeholder(placeholderService, geometricFiltersApply, placeholderGeodisambiguationShouldExecute),
controllers.placeholder(placeholderService, geometricFiltersDontApply, placeholderIdsLookupShouldExecute),
controllers.search_with_ids(peliasConfig.api, esclient, queries.address_using_ids, searchWithIdsShouldExecute),
// 3rd parameter is which query module to use, use fallback first, then
// use original search strategy if first query didn't return anything
controllers.search(peliasConfig.api, esclient, queries.cascading_fallback, fallbackQueryShouldExecute),
sanitizers.defer_to_addressit(shouldDeferToAddressIt),
controllers.search(peliasConfig.api, esclient, queries.very_old_prod, oldProdQueryShouldExecute),
postProc.trimByGranularity(),
postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig.api),

17
sanitizer/_text.js

@ -1,29 +1,20 @@
var check = require('check-types'),
text_analyzer = require('pelias-text-analyzer');
const check = require('check-types');
const _ = require('lodash');
// validate texts, convert types and apply defaults
function sanitize( raw, clean ){
// error & warning messages
var messages = { errors: [], warnings: [] };
const messages = { errors: [], warnings: [] };
// invalid input 'text'
// must call `!check.nonEmptyString` since `check.emptyString` returns
// `false` for `undefined` and `null`
if( !check.nonEmptyString( raw.text ) ){
messages.errors.push('invalid param \'text\': text length, must be >0');
}
// valid input 'text'
else {
// valid text
} else {
clean.text = raw.text;
// parse text with query parser
var parsed_text = text_analyzer.parse(clean.text);
if (check.assigned(parsed_text)) {
clean.parsed_text = parsed_text;
}
}
return messages;

1
sanitizer/_text_addressit.js

@ -20,6 +20,7 @@ function sanitize( raw, clean ){
// valid text
clean.text = raw.text;
clean.parser = 'addressit';
// remove anything that may have been parsed before
delete clean.parsed_text;

32
sanitizer/defer_to_addressit.js

@ -0,0 +1,32 @@
const sanitizeAll = require('../sanitizer/sanitizeAll'),
sanitizers = {
text: require('../sanitizer/_text_addressit')
};
const sanitize = function(req, cb) { sanitizeAll(req, sanitizers, cb); };
const logger = require('pelias-logger').get('api');
const logging = require( '../helper/logging' );
// middleware
module.exports = (should_execute) => {
return function(req, res, next) {
// if res.data already has results then don't call the _text_autocomplete sanitizer
// this has been put into place for when the libpostal integration way of querying
// ES doesn't return anything and we want to fallback to the old logic
if (!should_execute(req, res)) {
return next();
}
// log the query that caused a fallback since libpostal+new-queries didn't return anything
if (req.path === '/v1/search') {
const queryText = logging.isDNT(req) ? '[text removed]' : req.clean.text;
logger.info(`fallback queryText: ${queryText}`);
}
sanitize( req, function( err, clean ){
next();
});
};
};

2
sanitizer/search.js

@ -6,8 +6,6 @@ module.exports.middleware = (_api_pelias_config) => {
singleScalarParameters: require('../sanitizer/_single_scalar_parameters'),
quattroshapes_deprecation: require('../sanitizer/_deprecate_quattroshapes'),
text: require('../sanitizer/_text'),
iso2_to_iso3: require('../sanitizer/_iso2_to_iso3'),
city_name_standardizer: require('../sanitizer/_city_name_standardizer'),
size: require('../sanitizer/_size')(/* use defaults*/),
layers: require('../sanitizer/_targets')('layers', type_mapping.layer_mapping),
sources: require('../sanitizer/_targets')('sources', type_mapping.source_mapping),

30
sanitizer/search_fallback.js

@ -1,30 +0,0 @@
var sanitizeAll = require('../sanitizer/sanitizeAll'),
sanitizers = {
text: require('../sanitizer/_text_addressit')
};
var sanitize = function(req, cb) { sanitizeAll(req, sanitizers, cb); };
var logger = require('pelias-logger').get('api');
var logging = require( '../helper/logging' );
var _ = require('lodash');
// middleware
module.exports.middleware = function( req, res, next ){
// if res.data already has results then don't call the _text_autocomplete sanitizer
// this has been put into place for when the libpostal integration way of querying
// ES doesn't return anything and we want to fallback to the old logic
if (_.get(res, 'data', []).length > 0) {
return next();
}
// log the query that caused a fallback since libpostal+new-queries didn't return anything
if (req.path === '/v1/search') {
const queryText = logging.isDNT(req) ? '[text removed]' : req.clean.text;
logger.info(`fallback queryText: ${queryText}`);
}
sanitize( req, function( err, clean ){
next();
});
};

14
service/configurations/PlaceHolder.js

@ -12,9 +12,17 @@ class PlaceHolder extends ServiceConfiguration {
}
getParameters(req) {
const parameters = {
text: req.clean.text
};
const parameters = {};
if (_.has(req.clean.parsed_text, 'street')) {
// assemble all these fields into a space-delimited string
parameters.text = _.values(_.pick(req.clean.parsed_text,
['neighbourhood', 'borough', 'city', 'county', 'state', 'country'])).join(' ');
} else {
parameters.text = req.clean.text;
}
if (_.has(req.clean, 'lang.iso6393')) {
parameters.lang = req.clean.lang.iso6393;

290
test/unit/controller/libpostal.js

@ -0,0 +1,290 @@
'use strict';
const proxyquire = require('proxyquire').noCallThru();
const _ = require('lodash');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', t => {
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => undefined
}
});
t.equal(typeof controller, 'function', 'libpostal is a function');
t.equal(typeof controller(), 'function', 'libpostal returns a controller');
t.end();
});
};
module.exports.tests.should_execute = (test, common) => {
test('should_execute returning false should not call text-analyzer', t => {
const should_execute = (req, res) => {
// req and res should be passed to should_execute
t.deepEquals(req, {
clean: {
text: 'original query'
}
});
t.deepEquals(res, { b: 2 });
return false;
};
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => {
t.fail('parse should not have been called');
}
}
})(should_execute);
const req = {
clean: {
text: 'original query'
}
};
const res = { b: 2 };
controller(req, res, () => {
t.deepEquals(req, {
clean: {
text: 'original query'
}
}, 'req should not have been modified');
t.deepEquals(res, { b: 2 });
t.end();
});
});
test('should_execute returning false should not call text-analyzer', t => {
t.plan(5);
const should_execute = (req, res) => {
// req and res should be passed to should_execute
t.deepEquals(req, {
clean: {
text: 'original query'
}
});
t.deepEquals(res, { b: 2 });
return true;
};
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: (query) => {
t.equals(query, 'original query');
return undefined;
}
}
})(should_execute);
const req = {
clean: {
text: 'original query'
}
};
const res = { b: 2 };
controller(req, res, () => {
t.deepEquals(req, {
clean: {
text: 'original query'
}
}, 'req should not have been modified');
t.deepEquals(res, { b: 2 });
t.end();
});
});
};
module.exports.tests.parse_is_called = (test, common) => {
test('parse returning undefined should not overwrite clean.parsed_text', t => {
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => undefined
}
})(() => true);
const req = {
clean: {
parsed_text: 'original parsed_text'
}
};
const res = 'this is the response';
controller(req, res, () => {
t.deepEquals(req, {
clean: {
parsed_text: 'original parsed_text'
}
});
t.deepEquals(res, 'this is the response');
t.end();
});
});
test('parse returning something should overwrite clean.parsed_text', t => {
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => 'replacement parsed_text'
}
})(() => true);
const req = {
clean: {
parsed_text: 'original parsed_text'
}
};
const res = 'this is the response';
controller(req, res, () => {
t.deepEquals(req, {
clean: {
parsed_text: 'replacement parsed_text'
}
});
t.deepEquals(res, 'this is the response');
t.end();
});
});
};
module.exports.tests.iso2_conversion = (test, common) => {
test('no country in parse response should not leave country unset', t => {
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => ({
locality: 'this is the locality'
})
},
'iso3166-1': {
is2: () => t.fail('should not have been called'),
to3: () => t.fail('should not have been called')
}
})(() => true);
const req = {
clean: {
parsed_text: 'original parsed_text'
}
};
const res = 'this is the response';
controller(req, res, () => {
t.deepEquals(req, {
clean: {
parsed_text: {
locality: 'this is the locality'
}
}
});
t.deepEquals(res, 'this is the response');
t.end();
});
});
test('unknown country should not be converted', t => {
t.plan(3);
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => ({
country: 'unknown country code'
})
},
'iso3166-1': {
is2: country => {
t.equals(country, 'UNKNOWN COUNTRY CODE');
return false;
},
to3: () => t.fail('should not have been called')
}
})(() => true);
const req = {
clean: {
parsed_text: 'original parsed_text'
}
};
const res = 'this is the response';
controller(req, res, () => {
t.deepEquals(req, {
clean: {
parsed_text: {
country: 'unknown country code'
}
}
});
t.deepEquals(res, 'this is the response');
t.end();
});
});
test('ISO2 country should be converted to ISO3', t => {
t.plan(4);
const controller = proxyquire('../../../controller/libpostal', {
'pelias-text-analyzer': {
parse: () => ({
country: 'ISO2 COUNTRY CODE'
})
},
'iso3166-1': {
is2: country => {
t.equals(country, 'ISO2 COUNTRY CODE');
return true;
},
to3: country => {
t.equals(country, 'ISO2 COUNTRY CODE');
return 'ISO3 COUNTRY CODE';
}
}
})(() => true);
const req = {
clean: {
parsed_text: 'original parsed_text'
}
};
const res = 'this is the response';
controller(req, res, () => {
t.deepEquals(req, {
clean: {
parsed_text: {
country: 'ISO3 COUNTRY CODE'
}
}
});
t.deepEquals(res, 'this is the response');
t.end();
});
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /libpostal ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

619
test/unit/controller/placeholder.js

@ -30,7 +30,7 @@ module.exports.tests.should_execute = (test, common) => {
return false;
};
const controller = placeholder(placeholder_service, should_execute);
const controller = placeholder(placeholder_service, true, should_execute);
const req = { a: 1 };
const res = { b: 2 };
@ -52,7 +52,7 @@ module.exports.tests.should_execute = (test, common) => {
callback(null, []);
};
const controller = placeholder(placeholder_service, _.constant(true));
const controller = placeholder(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { b: 2 };
@ -206,7 +206,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -324,7 +324,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -387,7 +387,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -447,7 +447,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -512,7 +512,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -577,7 +577,7 @@ module.exports.tests.success = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -742,7 +742,7 @@ module.exports.tests.result_filtering = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
param1: 'param1 value',
@ -804,6 +804,141 @@ module.exports.tests.result_filtering = (test, common) => {
});
test('when geometric_filters_apply is false, boundary.rect should not apply', (t) => {
const logger = require('pelias-mock-logger')();
const placeholder_service = (req, callback) => {
t.deepEqual(req, {
param1: 'param1 value',
clean: {
'boundary.rect.min_lat': -1,
'boundary.rect.max_lat': 1,
'boundary.rect.min_lon': -1,
'boundary.rect.max_lon': 1
}
});
const response = [
{
// inside bbox
id: 1,
name: 'name 1',
placetype: 'neighbourhood',
geom: {
lat: 0,
lon: 0
}
},
{
// outside bbox
id: 2,
name: 'name 2',
placetype: 'neighbourhood',
geom: {
lat: -2,
lon: 2
}
},
{
// outside bbox
id: 3,
name: 'name 3',
placetype: 'neighbourhood',
geom: {
lat: 2,
lon: -2
}
}
];
callback(null, response);
};
const should_execute = (req, res) => {
return true;
};
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, false, () => true);
const req = {
param1: 'param1 value',
clean: {
'boundary.rect.min_lat': -1,
'boundary.rect.max_lat': 1,
'boundary.rect.min_lon': -1,
'boundary.rect.max_lon': 1
}
};
const res = { };
controller(req, res, () => {
const expected_res = {
meta: {
query_type: 'fallback'
},
data: [
{
_id: '1',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '1',
center_point: {
lat: 0,
lon: 0
},
name: {
'default': 'name 1'
},
phrase: {
'default': 'name 1'
}
},
{
_id: '2',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '2',
center_point: {
lat: -2,
lon: 2
},
name: {
'default': 'name 2'
},
phrase: {
'default': 'name 2'
}
},
{
_id: '3',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '3',
center_point: {
lat: 2,
lon: -2
},
name: {
'default': 'name 3'
},
phrase: {
'default': 'name 3'
}
}
]
};
t.deepEquals(res, expected_res);
t.end();
});
});
test('when boundary.circle is available, results outside of it should be removed', (t) => {
const logger = require('pelias-mock-logger')();
@ -927,7 +1062,7 @@ module.exports.tests.result_filtering = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
param1: 'param1 value',
@ -988,6 +1123,139 @@ module.exports.tests.result_filtering = (test, common) => {
});
test('when geometric_filters_apply is false, boundary.circle should not apply', (t) => {
const logger = require('pelias-mock-logger')();
const placeholder_service = (req, callback) => {
t.deepEqual(req, {
param1: 'param1 value',
clean: {
'boundary.circle.lat': 0,
'boundary.circle.lon': 0,
'boundary.circle.radius': 500
}
});
const response = [
{
// inside circle
id: 1,
name: 'name 1',
placetype: 'neighbourhood',
geom: {
lat: 1,
lon: 1
}
},
{
// outside circle on +lon
id: 2,
name: 'name 2',
placetype: 'neighbourhood',
geom: {
lat: -45,
lon: 45
}
},
{
// outside bbox on +lat
id: 3,
name: 'name 3',
placetype: 'neighbourhood',
geom: {
lat: 45,
lon: -45
}
}
];
callback(null, response);
};
const should_execute = (req, res) => {
return true;
};
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, false, () => true);
const req = {
param1: 'param1 value',
clean: {
'boundary.circle.lat': 0,
'boundary.circle.lon': 0,
'boundary.circle.radius': 500
}
};
const res = { };
controller(req, res, () => {
const expected_res = {
meta: {
query_type: 'fallback'
},
data: [
{
_id: '1',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '1',
center_point: {
lat: 1,
lon: 1
},
name: {
'default': 'name 1'
},
phrase: {
'default': 'name 1'
}
},
{
_id: '2',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '2',
center_point: {
lat: -45,
lon: 45
},
name: {
'default': 'name 2'
},
phrase: {
'default': 'name 2'
}
},
{
_id: '3',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '3',
center_point: {
lat: 45,
lon: -45
},
name: {
'default': 'name 3'
},
phrase: {
'default': 'name 3'
}
}
]
};
t.deepEquals(res, expected_res);
t.end();
});
});
test('only results matching explicit layers should be returned', (t) => {
const logger = mock_logger();
@ -1062,7 +1330,7 @@ module.exports.tests.result_filtering = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
param1: 'param1 value',
@ -1143,6 +1411,141 @@ module.exports.tests.result_filtering = (test, common) => {
});
test('if req.clean.parsed_text contains street, don\'t filter on anything', (t) => {
const logger = mock_logger();
const placeholder_service = (req, callback) => {
t.deepEqual(req, {
param1: 'param1 value',
clean: {
layers: ['neighbourhood'],
parsed_text: {
street: 'street value'
}
}
});
const response = [
{
id: 1,
name: 'name 1',
placetype: 'neighbourhood',
lineage: [ {} ],
geom: {
area: 1,
lat: 14.141414,
lon: 41.414141
}
},
{
id: 2,
name: 'name 2',
placetype: 'borough',
lineage: [ {} ],
geom: {
area: 2,
lat: 15.151515,
lon: 51.515151
}
},
{
id: 3,
name: 'name 3',
placetype: 'locality',
lineage: [ {} ],
geom: {
area: 3,
lat: 16.161616,
lon: 61.616161
}
}
];
callback(null, response);
};
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, true, () => true);
const req = {
param1: 'param1 value',
clean: {
layers: ['neighbourhood'],
parsed_text: {
street: 'street value'
}
}
};
const res = { };
controller(req, res, () => {
const expected_res = {
meta: {
query_type: 'fallback'
},
data: [
{
_id: '1',
_type: 'neighbourhood',
layer: 'neighbourhood',
source: 'whosonfirst',
source_id: '1',
center_point: {
lat: 14.141414,
lon: 41.414141
},
name: {
'default': 'name 1'
},
phrase: {
'default': 'name 1'
}
},
{
_id: '2',
_type: 'borough',
layer: 'borough',
source: 'whosonfirst',
source_id: '2',
center_point: {
lat: 15.151515,
lon: 51.515151
},
name: {
'default': 'name 2'
},
phrase: {
'default': 'name 2'
}
},
{
_id: '3',
_type: 'locality',
layer: 'locality',
source: 'whosonfirst',
source_id: '3',
center_point: {
lat: 16.161616,
lon: 61.616161
},
name: {
'default': 'name 3'
},
phrase: {
'default': 'name 3'
}
}
]
};
t.deepEquals(res, expected_res);
t.ok(logger.isInfoMessage('[controller:placeholder] [result_count:3]'));
t.end();
});
});
test('only synthesized docs matching explicit boundary.country should be returned', (t) => {
const logger = require('pelias-mock-logger')();
@ -1215,7 +1618,7 @@ module.exports.tests.result_filtering = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
param1: 'param1 value',
@ -1285,6 +1688,178 @@ module.exports.tests.result_filtering = (test, common) => {
});
test('when geometric_filters_apply is false, boundary.country should not apply', (t) => {
const logger = require('pelias-mock-logger')();
const placeholder_service = (req, callback) => {
t.deepEqual(req, {
param1: 'param1 value',
clean: {
'boundary.country': 'ABC'
}
});
const response = [
{
id: 1,
name: 'name 1',
placetype: 'locality',
lineage: [
{
country: {
id: 1,
name: 'country name 1',
abbr: 'ABC'
}
},
{
country: {
id: 2,
name: 'country name 2',
abbr: 'DEF'
}
}
],
geom: {
lat: 14.141414,
lon: 41.414141
}
},
{
id: 3,
name: 'name 3',
placetype: 'locality',
lineage: [
{
country: {
id: 3,
name: 'country name 3',
abbr: 'ABC'
}
}
],
geom: {
lat: 15.151515,
lon: 51.515151
}
},
{
id: 4,
name: 'name 4',
placetype: 'locality',
lineage: [
{
country: {
id: 4,
name: 'country name 4',
abbr: 'GHI'
}
}
],
geom: {
lat: 16.161616,
lon: 61.616161
}
}
];
callback(null, response);
};
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, false, () => true);
const req = {
param1: 'param1 value',
clean: {
'boundary.country': 'ABC'
}
};
const res = { };
controller(req, res, () => {
const expected_res = {
meta: {
query_type: 'fallback'
},
data: [
{
_id: '1',
_type: 'locality',
layer: 'locality',
source: 'whosonfirst',
source_id: '1',
center_point: {
lat: 14.141414,
lon: 41.414141
},
name: {
'default': 'name 1'
},
phrase: {
'default': 'name 1'
},
parent: {
country: ['country name 1', 'country name 2'],
country_id: ['1', '2'],
country_a: ['ABC', 'DEF']
}
},
{
_id: '3',
_type: 'locality',
layer: 'locality',
source: 'whosonfirst',
source_id: '3',
center_point: {
lat: 15.151515,
lon: 51.515151
},
name: {
'default': 'name 3'
},
phrase: {
'default': 'name 3'
},
parent: {
country: ['country name 3'],
country_id: ['3'],
country_a: ['ABC']
}
},
{
_id: '4',
_type: 'locality',
layer: 'locality',
source: 'whosonfirst',
source_id: '4',
center_point: {
lat: 16.161616,
lon: 61.616161
},
name: {
'default': 'name 4'
},
phrase: {
'default': 'name 4'
},
parent: {
country: ['country name 4'],
country_id: ['4'],
country_a: ['GHI']
}
}
]
};
t.deepEquals(res, expected_res);
t.ok(logger.isInfoMessage('[controller:placeholder] [result_count:3]'));
t.end();
});
});
};
module.exports.tests.lineage_errors = (test, common) => {
@ -1325,7 +1900,7 @@ module.exports.tests.lineage_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1399,7 +1974,7 @@ module.exports.tests.lineage_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1472,7 +2047,7 @@ module.exports.tests.lineage_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1532,7 +2107,7 @@ module.exports.tests.geometry_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1591,7 +2166,7 @@ module.exports.tests.centroid_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1651,7 +2226,7 @@ module.exports.tests.centroid_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1721,7 +2296,7 @@ module.exports.tests.boundingbox_errors = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = { param1: 'param1 value' };
const res = { };
@ -1773,7 +2348,7 @@ module.exports.tests.error_conditions = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
errors: []
@ -1802,7 +2377,7 @@ module.exports.tests.error_conditions = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
errors: []
@ -1827,7 +2402,7 @@ module.exports.tests.error_conditions = (test, common) => {
const controller = proxyquire('../../../controller/placeholder', {
'pelias-logger': logger
})(placeholder_service, _.constant(true));
})(placeholder_service, true, () => true);
const req = {
errors: []

94
test/unit/controller/predicates/has_any_parsed_text_property.js

@ -0,0 +1,94 @@
'use strict';
const _ = require('lodash');
const has_any_parsed_text_property = require('../../../../controller/predicates/has_any_parsed_text_property');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.ok(_.isFunction(has_any_parsed_text_property), 'has_any_parsed_text_property is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('defined request.clean.parsed_text.property should return true', (t) => {
const req = {
clean: {
parsed_text: {
property: 'value'
}
}
};
t.ok(has_any_parsed_text_property('property')(req));
t.end();
});
test('clean.parsed_text with any property should return true ', (t) => {
const req = {
clean: {
parsed_text: {
property2: 'value2',
property3: 'value3'
}
}
};
t.ok(has_any_parsed_text_property('property1', 'property3')(req));
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('undefined request should return false', (t) => {
t.notOk(has_any_parsed_text_property('property')());
t.end();
});
test('undefined request.clean should return false', (t) => {
const req = {};
t.notOk(has_any_parsed_text_property('property')(req));
t.end();
});
test('undefined request.clean.parsed_text should return false', (t) => {
const req = {
clean: {}
};
t.notOk(has_any_parsed_text_property('property')(req));
t.end();
});
test('request.clean.parsed_text with none of the supplied properties should return false', (t) => {
const req = {
clean: {
parsed_text: {}
}
};
t.notOk(has_any_parsed_text_property('property1', 'property2')(req));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /has_any_parsed_text_property ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

63
test/unit/controller/predicates/has_request_parameter.js

@ -0,0 +1,63 @@
'use strict';
const _ = require('lodash');
const has_request_parameter = require('../../../../controller/predicates/has_request_parameter');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', t => {
t.equal(typeof has_request_parameter, 'function', 'has_request_parameter is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('request with specified parameter should return true', t => {
[[], {}, 'string value', 17].forEach(val => {
const req = {
clean: {
'parameter name': val
}
};
t.ok(has_request_parameter('parameter name')(req));
});
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('request with undefined clean should return false', t => {
const req = {};
t.notOk(has_request_parameter('parameter name')(req));
t.end();
});
test('request.clean without specified parameter should return false', t => {
const req = {
clean: {}
};
t.notOk(has_request_parameter('parameter name')(req));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /has_request_parameter ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

73
test/unit/controller/predicates/is_addressit_parse.js

@ -0,0 +1,73 @@
'use strict';
const _ = require('lodash');
const is_addressit_parse = require('../../../../controller/predicates/is_addressit_parse');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', t => {
t.ok(_.isFunction(is_addressit_parse), 'is_addressit_parse is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('request.clean.parser=addressit should return true', t => {
const req = {
clean: {
parser: 'addressit'
}
};
t.ok(is_addressit_parse(req));
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('undefined request should return false', t => {
t.notOk(is_addressit_parse(undefined));
t.end();
});
test('undefined request.clean should return false', t => {
const req = {};
t.notOk(is_addressit_parse(req));
t.end();
});
test('undefined request.clean.parser should return false', t => {
const req = {
clean: {}
};
t.notOk(is_addressit_parse(req));
t.end();
});
test('non-\'addressit\' request.clean.parser should return false', t => {
const req = {
clean: {
parser: 'not addressit'
}
};
t.notOk(is_addressit_parse(req));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /is_addressit_parse ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

4
test/unit/controller/predicates/is_admin_only_analysis.js

@ -14,7 +14,7 @@ module.exports.tests.interface = (test, common) => {
module.exports.tests.true_conditions = (test, common) => {
test('parsed_text with admin-only properties should return true', (t) => {
['neighbourhood', 'borough', 'city', 'county', 'state', 'postalcode', 'country'].forEach((property) => {
['neighbourhood', 'borough', 'city', 'county', 'state', 'country'].forEach((property) => {
const req = {
clean: {
parsed_text: {}
@ -47,7 +47,7 @@ module.exports.tests.false_conditions = (test, common) => {
});
test('parsed_text with non-admin properties should return false', (t) => {
['number', 'street', 'query', 'category'].forEach((property) => {
['number', 'street', 'query', 'category', 'postalcode'].forEach((property) => {
const req = {
clean: {
parsed_text: {}

111
test/unit/controller/predicates/is_only_non_admin_layers.js

@ -0,0 +1,111 @@
'use strict';
const _ = require('lodash');
const is_only_non_admin_layers = require('../../../../controller/predicates/is_only_non_admin_layers');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', t => {
t.equal(typeof is_only_non_admin_layers, 'function', 'is_only_non_admin_layers is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('request with specified parameter should return true', t => {
[
['venue', 'address', 'street'],
['venue', 'address'],
['venue', 'street'],
['address', 'street'],
['venue'],
['address'],
['street']
].forEach(layers => {
const req = {
clean: {
layers: layers
}
};
t.ok(is_only_non_admin_layers(req));
});
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('request with undefined clean should return false', t => {
const req = {};
t.notOk(is_only_non_admin_layers(req));
t.end();
});
test('request.clean without layers parameter should return false', t => {
const req = {
clean: {}
};
t.notOk(is_only_non_admin_layers(req));
t.end();
});
test('request with empty layers should return false', t => {
const req = {
clean: {
layers: []
}
};
t.notOk(is_only_non_admin_layers(req));
t.end();
});
test('request.clean.layers without venue, address, or street should return false', t => {
const req = {
clean: {
layers: ['locality']
}
};
t.notOk(is_only_non_admin_layers(req));
t.end();
});
test('request.clean.layers with other layers besides venue, address, or street should return false', t => {
['venue', 'address', 'street'].forEach(non_admin_layer => {
const req = {
clean: {
layers: ['locality', non_admin_layer]
}
};
t.notOk(is_only_non_admin_layers(req));
});
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /is_only_non_admin_layers ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

107
test/unit/controller/predicates/is_request_sources_only_whosonfirst.js

@ -0,0 +1,107 @@
'use strict';
const _ = require('lodash');
const is_request_sources_only_whosonfirst = require('../../../../controller/predicates/is_request_sources_only_whosonfirst');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.ok(_.isFunction(is_request_sources_only_whosonfirst), 'is_request_sources_only_whosonfirst is a function');
t.end();
});
};
module.exports.tests.true_conditions = (test, common) => {
test('sources only \'whosonfirst\' should return true', (t) => {
const req = {
clean: {
sources: [
'whosonfirst'
]
}
};
t.ok(is_request_sources_only_whosonfirst(req));
t.end();
});
};
module.exports.tests.false_conditions = (test, common) => {
test('undefined req should return false', (t) => {
t.notOk(is_request_sources_only_whosonfirst(undefined));
t.end();
});
test('undefined req.clean should return false', (t) => {
const req = {};
t.notOk(is_request_sources_only_whosonfirst(req));
t.end();
});
test('undefined req.clean.sources should return false', (t) => {
const req = {
clean: {}
};
t.notOk(is_request_sources_only_whosonfirst(req));
t.end();
});
test('empty req.clean.sources should return false', (t) => {
const req = {
clean: {
sources: []
}
};
t.notOk(is_request_sources_only_whosonfirst(req));
t.end();
});
test('sources not \'whosonfirst\' should return false', (t) => {
const req = {
clean: {
sources: [
'not whosonfirst'
]
}
};
t.notOk(is_request_sources_only_whosonfirst(req));
t.end();
});
test('sources other than \'whosonfirst\' should return false', (t) => {
const req = {
clean: {
sources: [
'whosonfirst', 'not whosonfirst'
]
}
};
t.notOk(is_request_sources_only_whosonfirst(req));
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /is_request_sources_only_whosonfirst ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

569
test/unit/controller/search_with_ids.js

@ -0,0 +1,569 @@
'use strict';
const setup = require('../../../controller/search_with_ids');
const proxyquire = require('proxyquire').noCallThru();
const mocklogger = require('pelias-mock-logger');
const _ = require('lodash');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.ok(_.isFunction(setup), 'setup is a function');
t.ok(_.isFunction(setup()), 'setup returns a controller');
t.end();
});
};
module.exports.tests.success = (test, common) => {
test('successful request to search service should replace data and meta', (t) => {
t.plan(5);
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
type: 'this is the query type'
});
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
t.equal(esclient, 'this is the esclient');
t.deepEqual(cmd, {
index: 'indexName value',
searchType: 'dfs_query_then_fetch',
body: 'this is the query body'
});
const docs = [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
];
const meta = { key: 'replacement meta value' };
callback(undefined, docs, meta);
},
'pelias-logger': logger
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {
data: [
{ name: 'original result #1'},
{ name: 'original result #2'}
],
meta: {
key: 'original meta value'
}
};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res, {
data: [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
],
meta: {
key: 'replacement meta value',
query_type: 'this is the query type'
}
});
t.ok(logger.isInfoMessage('[controller:search] [queryType:this is the query type] [es_result_count:2]'));
t.end();
};
controller(req, res, next);
});
test('undefined meta should set empty object into res', (t) => {
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
type: 'this is the query type'
});
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
const docs = [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
];
callback(undefined, docs, undefined);
},
'pelias-logger': logger
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {
data: [
{ name: 'original result #1'},
{ name: 'original result #2'}
],
meta: {
key: 'original meta value'
}
};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res, {
data: [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
],
meta: {
query_type: 'this is the query type'
}
});
t.end();
};
controller(req, res, next);
});
test('undefined docs in response should not overwrite existing results', (t) => {
t.plan(1+3); // ensures that search service was called, then req+res+logger tests
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
type: 'this is the query type'
});
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
t.pass('search service was called');
const meta = { key: 'new value' };
callback(undefined, undefined, meta);
},
'pelias-logger': logger
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {
data: [
{ id: 1 },
{ id: 2 }
],
meta: {
key: 'value'
}
};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res, {
data: [
{ id: 1 },
{ id: 2 }
],
meta: { key: 'value' }
});
t.notOk(logger.isInfoMessage(/[controller:search] [queryType:this is the query type] [es_result_count:0]/));
t.end();
};
controller(req, res, next);
});
test('empty docs in response should not overwrite existing results', (t) => {
t.plan(4);
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
type: 'this is the query type'
});
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
t.pass('search service was called');
const meta = { key: 'value' };
callback(undefined, [], meta);
}
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {
data: [
{ name: 'pre-existing result #1' },
{ name: 'pre-existing result #2' }
],
meta: {
key: 'value'
}
};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res, {
data: [
{ name: 'pre-existing result #1' },
{ name: 'pre-existing result #2' }
],
meta: { key: 'value' }
});
t.notOk(logger.isInfoMessage(/[controller:search] [queryType:this is the query type] [es_result_count:0]/));
t.end();
};
controller(req, res, next);
});
test('successful request on retry to search service should log info message', (t) => {
t.plan(3+2+2); // 3 search service calls, 2 log messages, 1 req, 1 res
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
type: 'this is the query type'
});
let searchServiceCallCount = 0;
const timeoutError = {
status: 408,
displayName: 'RequestTimeout',
message: 'Request Timeout after 17ms'
};
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
t.pass('search service was called');
if (searchServiceCallCount < 2) {
// note that the searchService got called
searchServiceCallCount++;
callback(timeoutError);
} else {
const docs = [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
];
const meta = { key: 'replacement meta value' };
callback(undefined, docs, meta);
}
},
'pelias-logger': logger
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {
data: [
{ name: 'original result #1'},
{ name: 'original result #2'}
],
meta: {
key: 'original meta value'
}
};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res, {
data: [
{ name: 'replacement result #1'},
{ name: 'replacement result #2'}
],
meta: {
key: 'replacement meta value',
query_type: 'this is the query type'
}
});
t.ok(logger.isInfoMessage('[controller:search] [queryType:this is the query type] [es_result_count:2]'));
t.ok(logger.isInfoMessage('succeeded on retry 2'));
t.end();
};
controller(req, res, next);
});
};
module.exports.tests.service_errors = (test, common) => {
test('default # of request timeout retries should be 3', (t) => {
// test for 1 initial search service, 3 retries, 1 log messages, 1 req, and 1 res
t.plan(1 + 3 + 1 + 2);
const logger = mocklogger();
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({
body: 'this is the query body',
});
const timeoutError = {
status: 408,
displayName: 'RequestTimeout',
message: 'Request Timeout after 17ms'
};
// a controller that validates that the search service was called
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
// note that the searchService got called
t.pass('search service was called');
callback(timeoutError);
},
'pelias-logger': logger
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.ok(logger.getInfoMessages(), [
'request timed out on attempt 1, retrying',
'request timed out on attempt 2, retrying',
'request timed out on attempt 3, retrying'
]);
t.deepEqual(req, {
clean: {},
errors: [timeoutError.message],
warnings: []
});
t.deepEqual(res, {});
t.end();
};
controller(req, res, next);
});
test('explicit apiConfig.requestRetries should retry that many times', (t) => {
t.plan(1 + 17); // test for initial search service call and 17 retries
const config = {
indexName: 'indexName value',
requestRetries: 17
};
const esclient = 'this is the esclient';
const query = () => ({ });
const timeoutError = {
status: 408,
displayName: 'RequestTimeout',
message: 'Request Timeout after 17ms'
};
// a controller that validates that the search service was called
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
// note that the searchService got called
t.pass('search service was called');
callback(timeoutError);
}
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
controller(req, res, () => t.end() );
});
test('only status code 408 should be considered a retryable request', (t) => {
t.plan(2);
const config = {
indexName: 'indexName value',
requestRetries: 17
};
const esclient = 'this is the esclient';
const query = () => ({ });
const nonTimeoutError = {
status: 500,
displayName: 'InternalServerError',
message: 'an internal server error occurred'
};
// a controller that validates that the search service was called
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
// note that the searchService got called
t.pass('search service was called');
callback(nonTimeoutError);
}
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [nonTimeoutError.message],
warnings: []
});
t.end();
};
controller(req, res, next);
});
test('string error should not retry and be logged as-is', (t) => {
t.plan(2); // service call + error is in req.errors
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => ({ });
// a controller that validates that the search service was called
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': (esclient, cmd, callback) => {
// note that the searchService got called
t.pass('search service was called');
callback('this is an error string');
}
})(config, esclient, query, () => true );
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: ['this is an error string'],
warnings: []
});
t.end();
};
controller(req, res, next);
});
};
module.exports.tests.should_execute = (test, common) => {
test('should_execute returning false and empty req.errors should call next', (t) => {
const esclient = () => t.fail('esclient should not have been called');
const query = () => t.fail('query should not have been called');
const should_execute = () => false;
const controller = setup( {}, esclient, query, should_execute );
const req = { };
const res = { };
const next = () => {
t.deepEqual(res, { });
t.end();
};
controller(req, res, next);
});
};
module.exports.tests.undefined_query = (test, common) => {
test('query returning undefined should not call service', (t) => {
t.plan(0, 'test will fail if search service actually gets called');
// a function that returns undefined
const query = () => undefined;
const controller = proxyquire('../../../controller/search_with_ids', {
'../service/search': () => {
t.fail('search service should not have been called');
}
})(undefined, undefined, query, () => true );
const next = () => t.end();
controller({}, {}, next);
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`GET /search ${name}`, testFunction);
}
for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

27
test/unit/query/MockQuery.js

@ -0,0 +1,27 @@
'use strict';
module.exports = class MockQuery {
constructor() {
this._score_functions = [];
this._filter_functions = [];
}
render(vs) {
return {
vs: vs,
score_functions: this._score_functions,
filter_functions: this._filter_functions
};
}
score(view) {
this._score_functions.push(view);
return this;
}
filter(view) {
this._filter_functions.push(view);
return this;
}
};

553
test/unit/query/address_search_using_ids.js

@ -0,0 +1,553 @@
const generateQuery = require('../../../query/address_search_using_ids');
const _ = require('lodash');
const proxyquire = require('proxyquire').noCallThru();
const mock_logger = require('pelias-mock-logger');
const MockQuery = require('./MockQuery');
module.exports.tests = {};
module.exports.tests.interface = (test, common) => {
test('valid interface', (t) => {
t.ok(_.isFunction(generateQuery));
t.end();
});
};
// helper for canned views
const views = {
focus_only_function: () => 'focus_only_function',
boundary_country: 'boundary_country view',
boundary_circle: 'boundary_circle view',
boundary_rect: 'boundary_rect view',
sources: 'sources view'
};
module.exports.tests.base_query = (test, common) => {
test('basic', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
}
};
const res = {
data: []
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.type, 'fallback');
t.equals(generatedQuery.body.vs.var('input:housenumber').toString(), 'housenumber value');
t.equals(generatedQuery.body.vs.var('input:street').toString(), 'street value');
t.notOk(generatedQuery.body.vs.isset('sources'));
t.equals(generatedQuery.body.vs.var('size').toString(), 20);
t.deepEquals(generatedQuery.body.score_functions, [
'focus_only_function'
]);
t.deepEquals(generatedQuery.body.filter_functions, [
'boundary_country view',
'boundary_circle view',
'boundary_rect view',
'sources view'
]);
t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal]']);
t.end();
});
};
module.exports.tests.other_parameters = (test, common) => {
test('explicit size set', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
querySize: 'querySize value'
};
const res = {
data: []
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('size').toString(), 'querySize value');
t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal] [param:querySize]']);
t.end();
});
test('explicit sources set', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
sources: ['source 1', 'source 2']
};
const res = {
data: []
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.deepEquals(generatedQuery.body.vs.var('sources').toString(), ['source 1', 'source 2']);
t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal] [param:sources]']);
t.end();
});
};
module.exports.tests.granularity_bands = (test, common) => {
test('neighbourhood/borough/locality/localadmin granularity band', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
}
};
const res = {
data: [
{
layer: 'neighbourhood',
source_id: 1
},
{
layer: 'borough',
source_id: 2
},
{
layer: 'locality',
source_id: 3
},
{
layer: 'localadmin',
source_id: 4
},
{
layer: 'county',
source_id: 5
},
{
layer: 'macrocounty',
source_id: 6
},
{
layer: 'region',
source_id: 7
},
{
layer: 'macroregion',
source_id: 8
},
{
layer: 'dependency',
source_id: 9
},
{
layer: 'country',
source_id: 10
},
{
layer: 'neighbourhood',
source_id: 11
},
{
layer: 'borough',
source_id: 12
},
{
layer: 'locality',
source_id: 13
},
{
layer: 'localadmin',
source_id: 14
},
{
layer: 'county',
source_id: 15
},
{
layer: 'macrocounty',
source_id: 16
},
{
layer: 'region',
source_id: 17
},
{
layer: 'macroregion',
source_id: 18
},
{
layer: 'dependency',
source_id: 19
},
{
layer: 'country',
source_id: 20
}
]
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.deepEquals(generatedQuery.body.vs.var('input:layers').$, {
neighbourhood: [1, 11],
borough: [2, 12],
locality: [3, 13],
localadmin: [4, 14],
region: [7, 17],
macroregion: [8, 18],
dependency: [9, 19],
country: [10, 20]
});
t.end();
});
test('only band members with ids should be passed', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
}
};
const res = {
data: [
{
layer: 'neighbourhood',
source_id: 1
}
]
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.deepEquals(generatedQuery.body.vs.var('input:layers').$, {
neighbourhood: [1],
borough: [],
locality: [],
localadmin: [],
region: [],
macroregion: [],
dependency: [],
country: []
});
t.end();
});
test('county/macrocounty granularity band', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
}
};
const res = {
data: [
{
layer: 'county',
source_id: 1
},
{
layer: 'macrocounty',
source_id: 2
},
{
layer: 'county',
source_id: 4
},
{
layer: 'macrocounty',
source_id: 5
}
]
};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.deepEquals(generatedQuery.body.vs.var('input:layers').$, {
county: [1, 4],
macrocounty: [2, 5]
});
t.end();
});
};
module.exports.tests.boundary_filters = (test, common) => {
test('boundary.country available should add to query', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
'boundary.country': 'boundary.country value'
};
const res = {};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('boundary:country').toString(), 'boundary.country value');
t.end();
});
test('focus.point.lat/lon w/both numbers should add to query', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
'focus.point.lat': 12.121212,
'focus.point.lon': 21.212121
};
const res = {};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('focus:point:lat').toString(), 12.121212);
t.equals(generatedQuery.body.vs.var('focus:point:lon').toString(), 21.212121);
t.end();
});
test('boundary.rect with all numbers should add to query', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
'boundary.rect.min_lat': 12.121212,
'boundary.rect.max_lat': 13.131313,
'boundary.rect.min_lon': 21.212121,
'boundary.rect.max_lon': 31.313131
};
const res = {};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('boundary:rect:top').toString(), 13.131313);
t.equals(generatedQuery.body.vs.var('boundary:rect:right').toString(), 31.313131);
t.equals(generatedQuery.body.vs.var('boundary:rect:bottom').toString(), 12.121212);
t.equals(generatedQuery.body.vs.var('boundary:rect:left').toString(), 21.212121);
t.end();
});
test('boundary circle without radius should set radius to default', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
'boundary.circle.lat': 12.121212,
'boundary.circle.lon': 21.212121
};
const res = {};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('boundary:circle:lat').toString(), 12.121212);
t.equals(generatedQuery.body.vs.var('boundary:circle:lon').toString(), 21.212121);
t.equals(generatedQuery.body.vs.var('boundary:circle:radius').toString(), '50km');
t.end();
});
test('boundary circle with radius set radius to that value rounded', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
number: 'housenumber value',
street: 'street value'
},
'boundary.circle.lat': 12.121212,
'boundary.circle.lon': 21.212121,
'boundary.circle.radius': 17.6
};
const res = {};
const generateQuery = proxyquire('../../../query/address_search_using_ids', {
'pelias-logger': logger,
'pelias-query': {
layout: {
AddressesUsingIdsQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
});
const generatedQuery = generateQuery(clean, res);
t.equals(generatedQuery.body.vs.var('boundary:circle:lat').toString(), 12.121212);
t.equals(generatedQuery.body.vs.var('boundary:circle:lon').toString(), 21.212121);
t.equals(generatedQuery.body.vs.var('boundary:circle:radius').toString(), '18km');
t.end();
});
};
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape(`address_search_using_ids query ${name}`, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

7
test/unit/query/search.js

@ -284,7 +284,7 @@ module.exports.tests.city_state = function(test, common) {
var query = generate(clean);
t.notEqual(query, undefined, 'should not have returned undefined');
t.equal(query, undefined, 'should have returned undefined');
t.end();
});
@ -300,7 +300,7 @@ module.exports.tests.city_state = function(test, common) {
var query = generate(clean);
t.notEqual(query, undefined, 'should not have returned undefined');
t.equal(query, undefined, 'should have returned undefined');
t.end();
});
@ -458,7 +458,7 @@ module.exports.tests.city_country = function(test, common) {
var query = generate(clean);
t.notEqual(query, undefined, 'should not have returned undefined');
t.equal(query, undefined, 'should have returned undefined');
t.end();
});
@ -621,7 +621,6 @@ module.exports.tests.city_country = function(test, common) {
t.end();
});
};
module.exports.all = function (tape, common) {

10
test/unit/run.js

@ -13,14 +13,21 @@ var tests = [
require('./schema'),
require('./controller/coarse_reverse'),
require('./controller/index'),
require('./controller/libpostal'),
require('./controller/place'),
require('./controller/placeholder'),
require('./controller/search'),
require('./controller/search_with_ids'),
require('./controller/predicates/has_any_parsed_text_property'),
require('./controller/predicates/has_response_data'),
require('./controller/predicates/has_results_at_layers'),
require('./controller/predicates/has_request_parameter'),
require('./controller/predicates/has_request_errors'),
require('./controller/predicates/is_addressit_parse'),
require('./controller/predicates/is_admin_only_analysis'),
require('./controller/predicates/is_coarse_reverse'),
require('./controller/predicates/is_only_non_admin_layers'),
require('./controller/predicates/is_request_sources_only_whosonfirst'),
require('./controller/predicates/is_service_enabled'),
require('./helper/diffPlaces'),
require('./helper/geojsonify'),
@ -45,6 +52,7 @@ var tests = [
require('./middleware/trimByGranularity'),
require('./middleware/trimByGranularityStructured'),
require('./middleware/requestLanguage'),
require('./query/address_search_using_ids'),
require('./query/autocomplete'),
require('./query/autocomplete_defaults'),
require('./query/search_defaults'),
@ -83,7 +91,7 @@ var tests = [
require('./sanitizer/reverse'),
require('./sanitizer/sanitizeAll'),
require('./sanitizer/search'),
require('./sanitizer/search_fallback'),
require('./sanitizer/defer_to_addressit'),
require('./sanitizer/wrap'),
require('./service/configurations/PlaceHolder'),
require('./service/configurations/PointInPolygon'),

146
test/unit/sanitizer/_text.js

@ -1,152 +1,56 @@
var type_mapping = require('../../../helper/type_mapping');
var proxyquire = require('proxyquire').noCallThru();
const sanitizer = require('../../../sanitizer/_text');
module.exports.tests = {};
module.exports.tests.text_parser = function(test, common) {
test('non-empty raw.text should call analyzer and set clean.text and clean.parsed_text', function(t) {
var mock_analyzer_response = {
key1: 'value 1',
key2: 'value 2'
};
var sanitizer = proxyquire('../../../sanitizer/_text', {
'pelias-text-analyzer': { parse: function(query) {
return mock_analyzer_response;
}
}});
var raw = {
test('non-empty raw.text should call analyzer and set clean.text and not clean.parsed_text', t => {
const raw = {
text: 'raw input'
};
var clean = {
};
var expected_clean = {
text: raw.text,
parsed_text: mock_analyzer_response
};
var messages = sanitizer(raw, clean);
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, [], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
t.end();
});
test('empty raw.text should add error message', function(t) {
var sanitizer = proxyquire('../../../sanitizer/_text', {
'pelias-text-analyzer': { parse: function(query) {
throw new Error('analyzer should not have been called');
}
}});
var raw = {
text: ''
};
var clean = {
};
var expected_clean = {
};
var messages = sanitizer(raw, clean);
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, ['invalid param \'text\': text length, must be >0'], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
t.end();
});
test('undefined raw.text should add error message', function(t) {
var sanitizer = proxyquire('../../../sanitizer/_text', {
'pelias-text-analyzer': { parse: function(query) {
throw new Error('analyzer should not have been called');
}
}});
var raw = {
text: undefined
};
var clean = {
const clean = {
text: 'original clean.text'
};
var expected_clean = {
const expected_clean = {
text: raw.text
};
var messages = sanitizer(raw, clean);
const messages = sanitizer(raw, clean);
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, ['invalid param \'text\': text length, must be >0'], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
t.deepEquals(messages, { warnings: [], errors: [] }, 'no errors/warnings');
t.end();
});
test('text_analyzer.parse returning undefined should not overwrite clean.parsed_text', function(t) {
var sanitizer = proxyquire('../../../sanitizer/_text', {
'pelias-text-analyzer': { parse: function(query) {
return undefined;
}
}});
var raw = {
text: 'raw input'
};
var clean = {
parsed_text: 'original clean.parsed_text'
};
var expected_clean = {
text: raw.text,
parsed_text: 'original clean.parsed_text'
};
var messages = sanitizer(raw, clean);
test('undefined/empty raw.text should add error message', t => {
[undefined, ''].forEach(val => {
const raw = {
text: val
};
const clean = {
};
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, [], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
t.end();
const expected_clean = {
};
});
const messages = sanitizer(raw, clean);
test('text_analyzer.parse returning null should not overwrite clean.parsed_text', function(t) {
var sanitizer = proxyquire('../../../sanitizer/_text', {
'pelias-text-analyzer': { parse: function(query) {
return null;
}
}});
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, ['invalid param \'text\': text length, must be >0'], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
var raw = {
text: 'raw input'
};
var clean = {
parsed_text: 'original clean.parsed_text'
};
var expected_clean = {
text: raw.text,
parsed_text: 'original clean.parsed_text'
};
});
var messages = sanitizer(raw, clean);
t.deepEquals(clean, expected_clean);
t.deepEquals(messages.errors, [], 'no errors');
t.deepEquals(messages.warnings, [], 'no warnings');
t.end();
});
};
module.exports.all = function (tape, common) {
module.exports.all = (tape, common) => {
function test(name, testFunction) {
return tape('sanitizeR _text: ' + name, testFunction);
return tape(`sanitizer _text: ${name}`, testFunction);
}
for( var testCase in module.exports.tests ){

13
test/unit/sanitizer/_text_addressit.js

@ -33,6 +33,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: query.name + ', ' + query.admin_parts,
parser: 'addressit',
parsed_text: {
name: query.name,
regions: [ query.name ],
@ -57,6 +58,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: query.name + ',' + query.admin_parts,
parser: 'addressit',
parsed_text: {
name: query.name,
regions: [ query.name ],
@ -88,6 +90,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: query.name + ', ' + query.admin_parts,
parser: 'addressit',
parsed_text: {
name: query.name,
regions: [ query.name, query.admin_parts ],
@ -111,6 +114,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: query.name + ',' + query.admin_parts,
parser: 'addressit',
parsed_text: {
name: query.name,
regions: [ query.name, query.admin_parts ],
@ -136,6 +140,7 @@ module.exports.tests.text_parser = function(test, common) {
clean.parsed_text = 'this should be removed';
var expected_clean = {
parser: 'addressit',
text: 'yugolsavia'
};
@ -155,6 +160,7 @@ module.exports.tests.text_parser = function(test, common) {
clean.parsed_text = 'this should be removed';
var expected_clean = {
parser: 'addressit',
text: 'small town'
};
@ -174,6 +180,7 @@ module.exports.tests.text_parser = function(test, common) {
clean.parsed_text = 'this should be removed';
var expected_clean = {
parser: 'addressit',
text: '123 main'
};
@ -193,6 +200,7 @@ module.exports.tests.text_parser = function(test, common) {
clean.parsed_text = 'this should be removed';
var expected_clean = {
parser: 'addressit',
text: 'main 123'
};
@ -213,6 +221,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: 'main particle new york',
parser: 'addressit',
parsed_text: {
regions: [ 'main particle' ],
state: 'NY'
@ -235,6 +244,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: '123 main st new york ny',
parser: 'addressit',
parsed_text: {
number: '123',
street: 'main st',
@ -259,6 +269,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: '123 main st new york ny 10010',
parser: 'addressit',
parsed_text: {
number: '123',
street: 'main st',
@ -283,6 +294,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: '339 W Main St, Cheshire, 06410',
parser: 'addressit',
parsed_text: {
name: '339 W Main St',
number: '339',
@ -308,6 +320,7 @@ module.exports.tests.text_parser = function(test, common) {
var expected_clean = {
text: '339 W Main St,Lancaster,PA',
parser: 'addressit',
parsed_text: {
name: '339 W Main St',
number: '339',

132
test/unit/sanitizer/defer_to_addressit.js

@ -0,0 +1,132 @@
const proxyquire = require('proxyquire').noCallThru();
const mock_logger = require('pelias-mock-logger');
module.exports.tests = {};
module.exports.tests.sanitize = (test, common) => {
test('verify that no sanitizers were called when should_execute returns false', (t) => {
t.plan(1);
const logger = mock_logger();
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
const defer_to_addressit = proxyquire('../../../sanitizer/defer_to_addressit', {
'../sanitizer/_text_addressit': () => {
t.fail('_text_addressit should not have been called');
},
'pelias-logger': logger
})(() => false);
defer_to_addressit({}, {}, () => {
t.equals(logger.getInfoMessages().length, 0);
t.end();
});
});
test('verify that _text_addressit sanitizer was called when should_execute returns true', (t) => {
t.plan(2);
const logger = mock_logger();
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
const defer_to_addressit = proxyquire('../../../sanitizer/defer_to_addressit', {
'../sanitizer/_text_addressit': () => {
t.pass('_text_addressit should have been called');
return { errors: [], warnings: [] };
},
'pelias-logger': logger,
'../helper/logging': {
isDNT: () => false
}
})(() => true);
const req = {
path: '/v1/search',
clean: {
text: 'this is the query text'
}
};
defer_to_addressit(req, {}, () => {
t.deepEquals(logger.getInfoMessages(), ['fallback queryText: this is the query text']);
t.end();
});
});
test('query should not be logged if path != \'/v1/search\'', (t) => {
t.plan(2);
const logger = mock_logger();
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
const defer_to_addressit = proxyquire('../../../sanitizer/defer_to_addressit', {
'../sanitizer/_text_addressit': () => {
t.pass('_text_addressit should have been called');
return { errors: [], warnings: [] };
},
'pelias-logger': logger
})(() => true);
const req = {
path: 'not /v1/search',
clean: {
text: 'this is the query text'
}
};
defer_to_addressit(req, {}, () => {
t.deepEquals(logger.getInfoMessages(), []);
t.end();
});
});
test('query should be logged as [text removed] if private', (t) => {
t.plan(2);
const logger = mock_logger();
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
const defer_to_addressit = proxyquire('../../../sanitizer/defer_to_addressit', {
'../sanitizer/_text_addressit': () => {
t.pass('_text_addressit should have been called');
return { errors: [], warnings: [] };
},
'pelias-logger': logger,
'../helper/logging': {
isDNT: () => true
}
})(() => true);
const req = {
path: '/v1/search',
clean: {
text: 'this is the query text'
}
};
defer_to_addressit(req, {}, () => {
t.deepEquals(logger.getInfoMessages(), ['fallback queryText: [text removed]']);
t.end();
});
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape(`SANITIZE /defer_to_addressit ${name}`, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

10
test/unit/sanitizer/search.js

@ -22,14 +22,6 @@ module.exports.tests.sanitize = (test, common) => {
called_sanitizers.push('_text');
return { errors: [], warnings: [] };
},
'../sanitizer/_iso2_to_iso3': () => {
called_sanitizers.push('_iso2_to_iso3');
return { errors: [], warnings: [] };
},
'../sanitizer/_city_name_standardizer': () => {
called_sanitizers.push('_city_name_standardizer');
return { errors: [], warnings: [] };
},
'../sanitizer/_size': function() {
if (_.isEmpty(arguments)) {
return () => {
@ -105,8 +97,6 @@ module.exports.tests.sanitize = (test, common) => {
'_single_scalar_parameters',
'_deprecate_quattroshapes',
'_text',
'_iso2_to_iso3',
'_city_name_standardizer',
'_size',
'_targets/layers',
'_targets/sources',

185
test/unit/sanitizer/search_fallback.js

@ -1,185 +0,0 @@
var proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {};
module.exports.tests.sanitize = function(test, common) {
test('verify that all sanitizers were called as expected when `res` is undefined', function(t) {
var called_sanitizers = [];
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
var search = proxyquire('../../../sanitizer/search_fallback', {
'../sanitizer/_text_addressit': function() {
called_sanitizers.push('_text_addressit');
return { errors: [], warnings: [] };
}
});
var expected_sanitizers = [
'_text_addressit'
];
var req = {};
search.middleware(req, undefined, function(){
t.deepEquals(called_sanitizers, expected_sanitizers);
t.end();
});
});
test('verify that all sanitizers were called as expected when `res` has no `data` property', function(t) {
var called_sanitizers = [];
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
var search = proxyquire('../../../sanitizer/search_fallback', {
'../sanitizer/_text_addressit': function() {
called_sanitizers.push('_text_addressit');
return { errors: [], warnings: [] };
}
});
var expected_sanitizers = [
'_text_addressit'
];
var req = {};
var res = {};
search.middleware(req, res, function(){
t.deepEquals(called_sanitizers, expected_sanitizers);
t.end();
});
});
test('verify that all sanitizers were called as expected when res.data is empty', function(t) {
var called_sanitizers = [];
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
var search = proxyquire('../../../sanitizer/search_fallback', {
'../sanitizer/_text_addressit': function() {
called_sanitizers.push('_text_addressit');
return { errors: [], warnings: [] };
}
});
var expected_sanitizers = [
'_text_addressit'
];
var req = {};
var res = {
data: []
};
search.middleware(req, res, function(){
t.deepEquals(called_sanitizers, expected_sanitizers);
t.end();
});
});
test('non-empty res.data should not call the _text_autocomplete sanitizer', function(t) {
var called_sanitizers = [];
// rather than re-verify the functionality of all the sanitizers, this test just verifies that they
// were all called correctly
var search = proxyquire('../../../sanitizer/search_fallback', {
'../sanitizer/_text_autocomplete': function() {
throw new Error('_text_autocomplete sanitizer should not have been called');
}
});
var expected_sanitizers = [];
var req = {};
var res = {
data: [{}]
};
search.middleware(req, res, function(){
t.deepEquals(called_sanitizers, expected_sanitizers);
t.end();
});
});
test('req.clean.text should be logged when isDNT=false', (t) => {
const infoLog = [];
const search = proxyquire('../../../sanitizer/search_fallback', {
'pelias-logger': {
get: () => {
return {
info: (msg) => {
infoLog.push(msg);
}
};
}
},
'../helper/logging': {
isDNT: () => { return false; }
}
});
const req = {
path: '/v1/search',
clean: {
text: 'this is the query text'
}
};
search.middleware(req, undefined, () => {
t.deepEquals(infoLog, [`fallback queryText: ${req.clean.text}`]);
t.end();
});
});
test('req.clean.text should not be logged when isDNT=true', (t) => {
const infoLog = [];
const search = proxyquire('../../../sanitizer/search_fallback', {
'pelias-logger': {
get: () => {
return {
info: (msg) => {
infoLog.push(msg);
}
};
}
},
'../helper/logging': {
isDNT: () => { return true; }
}
});
const req = {
path: '/v1/search',
clean: {
text: 'this is the query text'
}
};
search.middleware(req, undefined, () => {
t.deepEquals(infoLog, ['fallback queryText: [text removed]']);
t.end();
});
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANITIZE /search_fallback ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

53
test/unit/service/configurations/PlaceHolder.js

@ -126,6 +126,59 @@ module.exports.tests.all = (test, common) => {
});
test('existence of street in req.clean.parsed_text should assemble text parameter from admin fields', (t) => {
const configBlob = {
url: 'http://localhost:1234',
};
const placeholder = new PlaceHolder(configBlob);
const req = {
clean: {
text: 'text value',
parsed_text: {
street: 'street value',
neighbourhood: 'neighbourhood value',
borough: 'borough value',
city: 'city value',
county: 'county value',
state: 'state value',
country: 'country value'
}
}
};
t.deepEquals(placeholder.getParameters(req), {
text: 'neighbourhood value borough value city value county value state value country value'
});
t.end();
});
test('existence of street in req.clean.parsed_text should assemble text parameter from defined admin fields', (t) => {
const configBlob = {
url: 'http://localhost:1234',
};
const placeholder = new PlaceHolder(configBlob);
const req = {
clean: {
text: 'text value',
parsed_text: {
street: 'street value',
country: 'country value'
}
}
};
t.deepEquals(placeholder.getParameters(req), {
text: 'country value'
});
t.end();
});
};
module.exports.all = (tape, common) => {

Loading…
Cancel
Save