Browse Source

Merge pull request #793 from pelias/staging

Merge staging into production
pull/1005/head
Stephen K Hess 8 years ago committed by GitHub
parent
commit
35e482494e
  1. 7
      app.js
  2. 66
      controller/place.js
  3. 81
      controller/search.js
  4. 6
      package.json
  5. 45
      routes/v1.js
  6. 7
      sanitizer/search_fallback.js
  7. 14
      schema.js
  8. 12
      service/mget.js
  9. 10
      service/search.js
  10. 14
      src/backend.js
  11. 16
      test/unit/app.js
  12. 403
      test/unit/controller/place.js
  13. 626
      test/unit/controller/search.js
  14. 16
      test/unit/middleware/parseBBox.js
  15. 251
      test/unit/mock/alpha3.json
  16. 20
      test/unit/mock/backend.js
  17. 13
      test/unit/mock/query.js
  18. 3
      test/unit/run.js
  19. 64
      test/unit/sanitizer/search_fallback.js
  20. 226
      test/unit/schema.js
  21. 35
      test/unit/service/mget.js
  22. 35
      test/unit/service/search.js
  23. 40
      test/unit/src/backend.js

7
app.js

@ -1,10 +1,7 @@
var app = require('express')(); var app = require('express')();
var peliasConfig = require( 'pelias-config' ).generate(); var peliasConfig = require( 'pelias-config' ).generate(require('./schema'));
// validate the configuration before attempting to load the app
require('./src/configValidation').validate(peliasConfig);
if( peliasConfig.api.accessLog ){ if( peliasConfig.api.accessLog ){
app.use( require( './middleware/access_log' ).createAccessLogger( peliasConfig.api.accessLog ) ); app.use( require( './middleware/access_log' ).createAccessLogger( peliasConfig.api.accessLog ) );
@ -23,7 +20,7 @@ var legacy = require('./routes/legacy');
legacy.addRoutes(app, peliasConfig.api); legacy.addRoutes(app, peliasConfig.api);
var v1 = require('./routes/v1'); var v1 = require('./routes/v1');
v1.addRoutes(app, peliasConfig.api); v1.addRoutes(app, peliasConfig);
/** ----------------------- error middleware ----------------------- **/ /** ----------------------- error middleware ----------------------- **/

66
controller/place.js

@ -1,44 +1,82 @@
var service = { mget: require('../service/mget') }; 'use strict';
var logger = require('pelias-logger').get('api');
function setup( config, backend ){ const _ = require('lodash');
const retry = require('retry');
// allow overriding of dependencies const mgetService = require('../service/mget');
backend = backend || require('../src/backend'); const logger = require('pelias-logger').get('api');
function controller( req, res, next ){ function requestHasErrors(request) {
return _.get(request, 'errors', []).length > 0;
}
function isRequestTimeout(err) {
return _.get(err, 'status') === 408;
}
// do not run controller when a request function setup( apiConfig, esclient ){
// validation error has occurred. function controller( req, res, next ){
if( req.errors && req.errors.length ){ // do not run controller when a request validation error has occurred.
if (requestHasErrors(req)){
return next(); return next();
} }
var query = req.clean.ids.map( function(id) { // 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);
const cmd = req.clean.ids.map( function(id) {
return { return {
_index: config.indexName, _index: apiConfig.indexName,
_type: id.layers, _type: id.layers,
_id: id.id _id: id.id
}; };
}); });
logger.debug( '[ES req]', query ); logger.debug( '[ES req]', cmd );
service.mget( backend, query, function( err, docs ) { operation.attempt((currentAttempt) => {
console.log('err:' + err); mgetService( esclient, cmd, function( err, docs ) {
// 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;
}
// error handler // error handler
if( err ){ if( err ){
if (_.isObject(err) && err.message) {
req.errors.push( err.message );
} else {
req.errors.push( err ); req.errors.push( err );
} }
}
// set response data // set response data
else { 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}`);
}
res.data = docs; res.data = docs;
} }
logger.debug('[ES response]', docs); logger.debug('[ES response]', docs);
next(); next();
}); });
});
} }
return controller; return controller;

81
controller/search.js

@ -1,54 +1,84 @@
var _ = require('lodash'); 'use strict';
var service = { search: require('../service/search') }; const _ = require('lodash');
var logger = require('pelias-logger').get('api');
var logging = require( '../helper/logging' );
function setup( config, backend, query ){ const searchService = require('../service/search');
const logger = require('pelias-logger').get('api');
const logging = require( '../helper/logging' );
const retry = require('retry');
// allow overriding of dependencies function requestHasErrors(request) {
backend = backend || require('../src/backend'); return _.get(request, 'errors', []).length > 0;
query = query || require('../query/search'); }
function responseHasData(response) {
return _.get(response, 'data', []).length > 0;
}
function isRequestTimeout(err) {
return _.get(err, 'status') === 408;
}
function setup( apiConfig, esclient, query ){
function controller( req, res, next ){ function controller( req, res, next ){
// do not run controller when a request // do not run controller when a request
// validation error has occurred. // validation error has occurred.
if( req.errors && req.errors.length ){ if (requestHasErrors(req)) {
return next(); return next();
} }
// do not run controller if there are already results // do not run controller if there are already results
// this was added during libpostal integration. if the libpostal parse/query // this was added during libpostal integration. if the libpostal parse/query
// doesn't return anything then fallback to old search-engine-y behavior // doesn't return anything then fallback to old search-engine-y behavior
if (res && res.hasOwnProperty('data') && res.data.length > 0) { if (responseHasData(res)) {
return next(); return next();
} }
var cleanOutput = _.cloneDeep(req.clean); let cleanOutput = _.cloneDeep(req.clean);
if (logging.isDNT(req)) { if (logging.isDNT(req)) {
cleanOutput = logging.removeFields(cleanOutput); cleanOutput = logging.removeFields(cleanOutput);
} }
// log clean parameters for stats // log clean parameters for stats
logger.info('[req]', 'endpoint=' + req.path, cleanOutput); logger.info('[req]', 'endpoint=' + req.path, cleanOutput);
var renderedQuery = query(req.clean); const renderedQuery = query(req.clean);
// if there's no query to call ES with, skip the service // if there's no query to call ES with, skip the service
if (_.isUndefined(renderedQuery)) { if (_.isUndefined(renderedQuery)) {
return next(); return next();
} }
// backend command // options for retry
var cmd = { // maxRetries is from the API config with default of 3
index: config.indexName, // 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', searchType: 'dfs_query_then_fetch',
body: renderedQuery.body body: renderedQuery.body
}; };
logger.debug( '[ES req]', cmd ); logger.debug( '[ES req]', cmd );
// query backend operation.attempt((currentAttempt) => {
service.search( backend, cmd, function( err, docs, meta ){ // 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;
}
// error handler // error handler
if( err ){ if( err ){
@ -60,18 +90,31 @@ function setup( config, backend, query ){
} }
// set response data // set response data
else { 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}`);
}
res.data = docs; res.data = docs;
res.meta = meta || {}; res.meta = meta || {};
// store the query_type for subsequent middleware // store the query_type for subsequent middleware
res.meta.query_type = renderedQuery.type; res.meta.query_type = renderedQuery.type;
logger.info(`[controller:search] [queryType:${renderedQuery.type}] [es_result_count:` + const messageParts = [
(res.data && res.data.length ? res.data.length : 0)); '[controller:search]',
`[queryType:${renderedQuery.type}]`,
`[es_result_count:${_.get(res, 'data', []).length}]`
];
logger.info(messageParts.join(' '));
} }
logger.debug('[ES response]', docs); logger.debug('[ES response]', docs);
next(); next();
}); });
});
} }
return controller; return controller;

6
package.json

@ -41,24 +41,24 @@
"elasticsearch": "^12.0.1", "elasticsearch": "^12.0.1",
"elasticsearch-exceptions": "0.0.4", "elasticsearch-exceptions": "0.0.4",
"express": "^4.8.8", "express": "^4.8.8",
"express-http-proxy": "^0.10.0", "express-http-proxy": "^0.11.0",
"extend": "3.0.0", "extend": "3.0.0",
"geojson": "^0.4.0", "geojson": "^0.4.0",
"geojson-extent": "^0.3.1", "geojson-extent": "^0.3.1",
"geolib": "^2.0.18", "geolib": "^2.0.18",
"geopipes-elasticsearch-backend": "^0.2.0",
"iso3166-1": "^0.2.3", "iso3166-1": "^0.2.3",
"joi": "^10.1.0", "joi": "^10.1.0",
"lodash": "^4.5.0", "lodash": "^4.5.0",
"markdown": "0.5.0", "markdown": "0.5.0",
"morgan": "1.7.0", "morgan": "1.7.0",
"pelias-categories": "1.1.0", "pelias-categories": "1.1.0",
"pelias-config": "2.6.0", "pelias-config": "2.7.1",
"pelias-labels": "1.5.1", "pelias-labels": "1.5.1",
"pelias-logger": "0.1.0", "pelias-logger": "0.1.0",
"pelias-model": "4.4.0", "pelias-model": "4.4.0",
"pelias-query": "8.12.0", "pelias-query": "8.12.0",
"pelias-text-analyzer": "1.7.0", "pelias-text-analyzer": "1.7.0",
"retry": "^0.10.1",
"stats-lite": "2.0.3", "stats-lite": "2.0.3",
"superagent": "^3.2.1", "superagent": "^3.2.1",
"through2": "^2.0.3" "through2": "^2.0.3"

45
routes/v1.js

@ -1,6 +1,5 @@
var express = require('express');
var Router = require('express').Router; var Router = require('express').Router;
var reverseQuery = require('../query/reverse'); var elasticsearch = require('elasticsearch');
/** ----------------------- sanitizers ----------------------- **/ /** ----------------------- sanitizers ----------------------- **/
var sanitizers = { var sanitizers = {
@ -30,7 +29,9 @@ var controllers = {
var queries = { var queries = {
libpostal: require('../query/search'), libpostal: require('../query/search'),
fallback_to_old_prod: require('../query/search_original'), fallback_to_old_prod: require('../query/search_original'),
structured_geocoding: require('../query/structured_geocoding') structured_geocoding: require('../query/structured_geocoding'),
reverse: require('../query/reverse'),
autocomplete: require('../query/autocomplete')
}; };
/** ----------------------- controllers ----------------------- **/ /** ----------------------- controllers ----------------------- **/
@ -61,6 +62,7 @@ var postProc = {
* @param {object} peliasConfig * @param {object} peliasConfig
*/ */
function addRoutes(app, peliasConfig) { function addRoutes(app, peliasConfig) {
const esclient = elasticsearch.Client(peliasConfig.esclient);
var base = '/v1/'; var base = '/v1/';
@ -68,23 +70,22 @@ function addRoutes(app, peliasConfig) {
var routers = { var routers = {
index: createRouter([ index: createRouter([
controllers.mdToHTML(peliasConfig, './public/apiDoc.md') controllers.mdToHTML(peliasConfig.api, './public/apiDoc.md')
]), ]),
attribution: createRouter([ attribution: createRouter([
controllers.mdToHTML(peliasConfig, './public/attribution.md') controllers.mdToHTML(peliasConfig.api, './public/attribution.md')
]), ]),
search: createRouter([ search: createRouter([
sanitizers.search.middleware, sanitizers.search.middleware,
middleware.calcSize(), middleware.calcSize(),
// 2nd parameter is `backend` which gets initialized internally
// 3rd parameter is which query module to use, use fallback/geodisambiguation // 3rd parameter is which query module to use, use fallback/geodisambiguation
// first, then use original search strategy if first query didn't return anything // first, then use original search strategy if first query didn't return anything
controllers.search(peliasConfig, undefined, queries.libpostal), controllers.search(peliasConfig.api, esclient, queries.libpostal),
sanitizers.search_fallback.middleware, sanitizers.search_fallback.middleware,
controllers.search(peliasConfig, undefined, queries.fallback_to_old_prod), controllers.search(peliasConfig.api, esclient, queries.fallback_to_old_prod),
postProc.trimByGranularity(), postProc.trimByGranularity(),
postProc.distances('focus.point.'), postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig), postProc.confidenceScores(peliasConfig.api),
postProc.confidenceScoresFallback(), postProc.confidenceScoresFallback(),
postProc.dedupe(), postProc.dedupe(),
postProc.interpolate(), postProc.interpolate(),
@ -94,16 +95,16 @@ function addRoutes(app, peliasConfig) {
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
structured: createRouter([ structured: createRouter([
sanitizers.structured_geocoding.middleware, sanitizers.structured_geocoding.middleware,
middleware.calcSize(), middleware.calcSize(),
controllers.search(peliasConfig, undefined, queries.structured_geocoding), controllers.search(peliasConfig.api, esclient, queries.structured_geocoding),
postProc.trimByGranularityStructured(), postProc.trimByGranularityStructured(),
postProc.distances('focus.point.'), postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig), postProc.confidenceScores(peliasConfig.api),
postProc.confidenceScoresFallback(), postProc.confidenceScoresFallback(),
postProc.dedupe(), postProc.dedupe(),
postProc.interpolate(), postProc.interpolate(),
@ -113,14 +114,14 @@ function addRoutes(app, peliasConfig) {
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
autocomplete: createRouter([ autocomplete: createRouter([
sanitizers.autocomplete.middleware, sanitizers.autocomplete.middleware,
controllers.search(peliasConfig, null, require('../query/autocomplete')), controllers.search(peliasConfig.api, esclient, queries.autocomplete),
postProc.distances('focus.point.'), postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig), postProc.confidenceScores(peliasConfig.api),
postProc.dedupe(), postProc.dedupe(),
postProc.accuracy(), postProc.accuracy(),
postProc.localNamingConventions(), postProc.localNamingConventions(),
@ -128,13 +129,13 @@ function addRoutes(app, peliasConfig) {
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
reverse: createRouter([ reverse: createRouter([
sanitizers.reverse.middleware, sanitizers.reverse.middleware,
middleware.calcSize(), middleware.calcSize(),
controllers.search(peliasConfig, undefined, reverseQuery), controllers.search(peliasConfig.api, esclient, queries.reverse),
postProc.distances('point.'), postProc.distances('point.'),
// reverse confidence scoring depends on distance from origin // reverse confidence scoring depends on distance from origin
// so it must be calculated first // so it must be calculated first
@ -146,13 +147,13 @@ function addRoutes(app, peliasConfig) {
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
nearby: createRouter([ nearby: createRouter([
sanitizers.nearby.middleware, sanitizers.nearby.middleware,
middleware.calcSize(), middleware.calcSize(),
controllers.search(peliasConfig, undefined, reverseQuery), controllers.search(peliasConfig.api, esclient, queries.reverse),
postProc.distances('point.'), postProc.distances('point.'),
// reverse confidence scoring depends on distance from origin // reverse confidence scoring depends on distance from origin
// so it must be calculated first // so it must be calculated first
@ -164,19 +165,19 @@ function addRoutes(app, peliasConfig) {
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
place: createRouter([ place: createRouter([
sanitizers.place.middleware, sanitizers.place.middleware,
controllers.place(peliasConfig), controllers.place(peliasConfig.api, esclient),
postProc.accuracy(), postProc.accuracy(),
postProc.localNamingConventions(), postProc.localNamingConventions(),
postProc.renamePlacenames(), postProc.renamePlacenames(),
postProc.parseBoundingBox(), postProc.parseBoundingBox(),
postProc.normalizeParentIds(), postProc.normalizeParentIds(),
postProc.assignLabels(), postProc.assignLabels(),
postProc.geocodeJSON(peliasConfig, base), postProc.geocodeJSON(peliasConfig.api, base),
postProc.sendJSON postProc.sendJSON
]), ]),
status: createRouter([ status: createRouter([

7
sanitizer/search_fallback.js

@ -6,20 +6,21 @@ var sanitizeAll = require('../sanitizer/sanitizeAll'),
var sanitize = function(req, cb) { sanitizeAll(req, sanitizers, cb); }; var sanitize = function(req, cb) { sanitizeAll(req, sanitizers, cb); };
var logger = require('pelias-logger').get('api'); var logger = require('pelias-logger').get('api');
var logging = require( '../helper/logging' ); var logging = require( '../helper/logging' );
var _ = require('lodash');
// middleware // middleware
module.exports.middleware = function( req, res, next ){ module.exports.middleware = function( req, res, next ){
// if res.data already has results then don't call the _text_autocomplete sanitizer // 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 // 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 // ES doesn't return anything and we want to fallback to the old logic
if (res && res.hasOwnProperty('data') && res.data.length > 0) { if (_.get(res, 'data', []).length > 0) {
return next(); return next();
} }
// log the query that caused a fallback since libpostal+new-queries didn't return anything // log the query that caused a fallback since libpostal+new-queries didn't return anything
if (req.path === '/v1/search') { if (req.path === '/v1/search') {
var queryText = logging.isDNT(req) ? '[text removed]' : req.clean.text; const queryText = logging.isDNT(req) ? '[text removed]' : req.clean.text;
logger.info(queryText); logger.info(`fallback queryText: ${queryText}`);
} }
sanitize( req, function( err, clean ){ sanitize( req, function( err, clean ){

14
src/configValidation.js → schema.js

@ -14,7 +14,7 @@ const Joi = require('joi');
// * api.relativeScores (boolean) // * api.relativeScores (boolean)
// * api.legacyUrl (string) // * api.legacyUrl (string)
// * api.localization (flipNumberAndStreetCountries is array of 3 character strings) // * api.localization (flipNumberAndStreetCountries is array of 3 character strings)
const schema = Joi.object().keys({ module.exports = Joi.object().keys({
api: Joi.object().keys({ api: Joi.object().keys({
version: Joi.string(), version: Joi.string(),
indexName: Joi.string(), indexName: Joi.string(),
@ -22,6 +22,7 @@ const schema = Joi.object().keys({
legacyUrl: Joi.string(), legacyUrl: Joi.string(),
accessLog: Joi.string(), accessLog: Joi.string(),
relativeScores: Joi.boolean(), relativeScores: Joi.boolean(),
requestRetries: Joi.number().integer().min(0),
localization: Joi.object().keys({ localization: Joi.object().keys({
flipNumberAndStreetCountries: Joi.array().items(Joi.string().regex(/^[A-Z]{3}$/)) flipNumberAndStreetCountries: Joi.array().items(Joi.string().regex(/^[A-Z]{3}$/))
}).unknown(false) }).unknown(false)
@ -31,14 +32,3 @@ const schema = Joi.object().keys({
requestTimeout: Joi.number().integer().min(0) requestTimeout: Joi.number().integer().min(0)
}).unknown(true) }).unknown(true)
}).requiredKeys('api', 'esclient').unknown(true); }).requiredKeys('api', 'esclient').unknown(true);
module.exports = {
validate: function validate(config) {
Joi.validate(config, schema, (err) => {
if (err) {
throw new Error(err.details[0].message);
}
});
}
};

12
service/mget.js

@ -13,26 +13,26 @@
var logger = require( 'pelias-logger' ).get( 'api' ); var logger = require( 'pelias-logger' ).get( 'api' );
function service( backend, query, cb ){ function service( esclient, query, cb ){
// backend command // elasticsearch command
var cmd = { var cmd = {
body: { body: {
docs: query docs: query
} }
}; };
// query new backend // query elasticsearch
backend().client.mget( cmd, function( err, data ){ esclient.mget( cmd, function( err, data ){
// log total ms elasticsearch reported the query took to execute // log total ms elasticsearch reported the query took to execute
if( data && data.took ){ if( data && data.took ){
logger.verbose( 'time elasticsearch reported:', data.took / 1000 ); logger.verbose( 'time elasticsearch reported:', data.took / 1000 );
} }
// handle backend errors // handle elasticsearch errors
if( err ){ if( err ){
logger.error( 'backend error', err ); logger.error( `elasticsearch error ${err}`);
return cb( err ); return cb( err );
} }

10
service/search.js

@ -7,19 +7,19 @@
var logger = require( 'pelias-logger' ).get( 'api' ); var logger = require( 'pelias-logger' ).get( 'api' );
function service( backend, cmd, cb ){ function service( esclient, cmd, cb ){
// query new backend // query elasticsearch
backend().client.search( cmd, function( err, data ){ esclient.search( cmd, function( err, data ){
// log total ms elasticsearch reported the query took to execute // log total ms elasticsearch reported the query took to execute
if( data && data.took ){ if( data && data.took ){
logger.verbose( 'time elasticsearch reported:', data.took / 1000 ); logger.verbose( 'time elasticsearch reported:', data.took / 1000 );
} }
// handle backend errors // handle elasticsearch errors
if( err ){ if( err ){
logger.error( 'backend error', err ); logger.error( `elasticsearch error ${err}` );
return cb( err ); return cb( err );
} }

14
src/backend.js

@ -1,14 +0,0 @@
var config = require( 'pelias-config' ).generate().esclient;
var Backend = require('geopipes-elasticsearch-backend'),
client = require('elasticsearch').Client(config),
backends = {};
function getBackend( index, type ){
var key = ( index + ':' + type );
if( !backends[key] ){
backends[key] = new Backend( client, index, type );
}
return backends[key];
}
module.exports = getBackend;

16
test/unit/app.js

@ -4,12 +4,16 @@ const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.invalid_configuration = function(test, common) { module.exports.tests.invalid_configuration = (test, common) => {
test('configuration validation throwing error should rethrow', function(t) { test('configuration validation throwing error should rethrow', (t) => {
t.throws(function() { t.throws(() => {
proxyquire('../../app', { proxyquire('../../app', {
'./src/configValidation': { './schema': 'this is the schema',
validate: () => { 'pelias-config': {
generate: (schema) => {
// the schema passed to generate should be the require'd schema
t.equals(schema, 'this is the schema');
throw Error('config is not valid'); throw Error('config is not valid');
} }
} }
@ -23,7 +27,7 @@ module.exports.tests.invalid_configuration = function(test, common) {
}; };
module.exports.all = function (tape, common) { module.exports.all = (tape, common) => {
function test(name, testFunction) { function test(name, testFunction) {
return tape('app: ' + name, testFunction); return tape('app: ' + name, testFunction);

403
test/unit/controller/place.js

@ -1,129 +1,372 @@
var setup = require('../../../controller/place'), 'use strict';
mockBackend = require('../mock/backend');
const setup = require('../../../controller/search');
const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.interface = function(test, common) { module.exports.tests.interface = (test, common) => {
test('valid interface', function(t) { test('valid interface', (t) => {
t.equal(typeof setup, 'function', 'setup is a function'); t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller'); t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end(); t.end();
}); });
}; };
// reminder: this is only the api subsection of the full config module.exports.tests.success = (test, common) => {
var fakeDefaultConfig = { test('successful request to search service should set data and meta', (t) => {
indexName: 'pelias' const config = {
indexName: 'indexName value'
}; };
const esclient = 'this is the esclient';
// functionally test controller (backend success) // request timeout messages willl be written here
module.exports.tests.functional_success = function(test, common) { const infoMesssages = [];
// expected geojson features for 'client/place/ok/1' fixture // a controller that validates the esclient and cmd that was passed to the search service
var expected = [{ const controller = proxyquire('../../../controller/place', {
type: 'Feature', '../service/mget': (esclient, query, callback) => {
geometry: { t.equal(esclient, 'this is the esclient');
type: 'Point', t.deepEqual(query, [
coordinates: [ -50.5, 100.1 ] {
}, _index: 'indexName value',
properties: { _type: 'layer1',
id: 'myid1', _id: 'id1'
layer: 'mytype1',
text: 'test name1, city1, state1'
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -51.5, 100.2 ]
}, },
properties: { {
id: 'myid2', _index: 'indexName value',
layer: 'mytype2', _type: 'layer2',
text: 'test name2, city2, state2' _id: 'id2'
} }
}]; ]);
test('functional success', function(t) { const docs = [{}, {}];
var backend = mockBackend( 'client/mget/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: [ 'a' ] } ] } }, 'correct backend command'); callback(undefined, docs);
}); }
var controller = setup( fakeDefaultConfig, backend ); })(config, esclient);
var res = {
status: function( code ){ const req = {
t.equal(code, 200, 'status set'); clean: {
return res; ids: [
{
id: 'id1',
layers: 'layer1'
}, },
json: function( json ){ {
t.equal(typeof json, 'object', 'returns json'); id: 'id2',
t.equal(typeof json.date, 'number', 'date set'); layers: 'layer2'
t.equal(json.type, 'FeatureCollection', 'valid geojson');
t.true(Array.isArray(json.features), 'features is array');
t.deepEqual(json.features, expected, 'values correctly mapped');
} }
]
},
errors: [],
warnings: []
}; };
var req = { clean: { ids: [ {'id' : 123, layers: [ 'a' ] } ] }, errors: [], warnings: [] }; const res = {};
var next = function next() {
t.equal(req.errors.length, 0, 'next was called without error'); const next = () => {
t.deepEqual(req.errors, []);
t.deepEqual(req.warnings, []);
t.deepEquals(res.data, [{}, {}]);
t.end(); t.end();
}; };
controller(req, res, next); controller(req, res, next);
}); });
test('functional success with custom index name', function(t) {
var fakeCustomizedConfig = {
indexName: 'alternateindexname'
}; };
var backend = mockBackend( 'client/mget/ok/1', function( cmd ){ module.exports.tests.error_conditions = (test, common) => {
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'alternateindexname', _type: [ 'a' ] } ] } }, 'correct backend command'); test('non-empty req.errors should ', (t) => {
const esclient = () => {
throw new Error('esclient should not have been called');
};
const controller = setup( {}, esclient );
// the existence of `errors` means that a sanitizer detected an error,
// so don't call the esclient
const req = {
errors: ['error']
};
const res = { };
t.doesNotThrow(() => {
controller(req, res, () => {});
});
t.end();
});
test('mgetService returning error should add to req.errors and ignore docs', (t) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
// request timeout messages willl be written here
const infoMesssages = [];
const nonTimeoutError = {
status: 500,
displayName: 'InternalServerError',
message: 'an internal server error occurred'
};
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/place', {
'../service/mget': (esclient, query, callback) => {
const docs = [{}, {}];
callback(nonTimeoutError, docs);
}
})(config, esclient);
const req = {
clean: {
ids: [
{
id: 'id1',
layers: 'layer1'
}
]
},
errors: [],
warnings: []
};
const res = {};
const next = () => {
t.deepEqual(req.errors, [nonTimeoutError.message]);
t.deepEqual(req.warnings, []);
t.deepEquals(res.data, undefined);
t.end();
};
controller(req, res, next);
}); });
var controller = setup( fakeCustomizedConfig, backend );
var res = { };
status: function( code ){
t.equal(code, 200, 'status set'); module.exports.tests.timeout = function(test, common) {
return res; test('default # of request timeout retries should be 3', (t) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
let searchServiceCallCount = 0;
const timeoutError = {
status: 408,
displayName: 'RequestTimeout',
message: 'Request Timeout after 17ms'
};
// request timeout messages willl be written here
const infoMesssages = [];
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/place', {
'../service/mget': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(timeoutError);
},
'pelias-logger': {
get: (service) => {
t.equal(service, 'api');
return {
info: (msg) => {
infoMesssages.push(msg);
}, },
json: function( json ){ debug: () => {}
t.equal(typeof json, 'object', 'returns json'); };
t.equal(typeof json.date, 'number', 'date set'); }
t.equal(json.type, 'FeatureCollection', 'valid geojson'); }
t.true(Array.isArray(json.features), 'features is array'); })(config, esclient);
t.deepEqual(json.features, expected, 'values correctly mapped');
const req = {
clean: {
ids: [
{
id: 'id1',
layers: 'layer1'
}
]
},
errors: [],
warnings: []
};
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 3+1);
t.ok(infoMesssages.indexOf('request timed out on attempt 1, retrying') !== -1);
t.ok(infoMesssages.indexOf('request timed out on attempt 2, retrying') !== -1);
t.ok(infoMesssages.indexOf('request timed out on attempt 3, retrying') !== -1);
t.deepEqual(req.errors, [timeoutError.message]);
t.deepEqual(res, {});
t.end();
};
controller(req, res, next);
});
test('explicit apiConfig.requestRetries should retry that many times', (t) => {
const config = {
indexName: 'indexName value',
requestRetries: 17
};
const esclient = 'this is the esclient';
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/place', {
'../service/mget': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(timeoutError);
}
})(config, esclient);
const req = {
clean: {
ids: [
{
id: 'id1',
layers: 'layer1'
} }
]
},
errors: [],
warnings: []
}; };
var req = { clean: { ids: [ {'id' : 123, layers: [ 'a' ] } ] }, errors: [], warnings: [] }; const res = {};
var next = function next() {
t.equal(req.errors.length, 0, 'next was called without error'); const next = () => {
t.equal(searchServiceCallCount, 17+1);
t.end(); t.end();
}; };
controller(req, res, next); controller(req, res, next);
}); });
test('only status code 408 should be considered a retryable request', (t) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
let searchServiceCallCount = 0;
const nonTimeoutError = {
status: 500,
displayName: 'InternalServerError',
message: 'an internal server error occurred'
};
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/place', {
'../service/mget': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(nonTimeoutError);
}
})(config, esclient);
const req = {
clean: {
ids: [
{
id: 'id1',
layers: 'layer1'
}
]
},
errors: [],
warnings: []
};
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 1);
t.deepEqual(req.errors, [nonTimeoutError.message]);
t.end();
}; };
// functionally test controller (backend failure) controller(req, res, next);
module.exports.tests.functional_failure = function(test, common) {
test('functional failure', function(t) {
var backend = mockBackend( 'client/mget/fail/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: ['b'] } ] } }, 'correct backend command');
}); });
var controller = setup( fakeDefaultConfig, backend );
var req = { clean: { ids: [ {'id' : 123, layers: [ 'b' ] } ] }, errors: [], warnings: [] }; test('string error should not retry and be logged as-is', (t) => {
var next = function( message ){ const config = {
t.equal(req.errors[0],'a backend error occurred','error passed to errorHandler'); indexName: 'indexName value'
};
const esclient = 'this is the esclient';
let searchServiceCallCount = 0;
const stringTypeError = 'this is an error string';
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/place', {
'../service/mget': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(stringTypeError);
}
})(config, esclient);
const req = {
clean: {
ids: [
{
id: 'id1',
layers: 'layer1'
}
]
},
errors: [],
warnings: []
};
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 1);
t.deepEqual(req.errors, [stringTypeError]);
t.end(); t.end();
}; };
controller(req, undefined, next );
controller(req, res, next);
}); });
}; };
module.exports.all = function (tape, common) { module.exports.all = (tape, common) => {
function test(name, testFunction) { function test(name, testFunction) {
return tape('GET /place ' + name, testFunction); return tape('GET /place ' + name, testFunction);
} }
for( var testCase in module.exports.tests ){ for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common); module.exports.tests[testCase](test, common);
} }
}; };

626
test/unit/controller/search.js

@ -1,7 +1,7 @@
var setup = require('../../../controller/search'), 'use strict';
mockBackend = require('../mock/backend'),
mockQuery = require('../mock/query'); const setup = require('../../../controller/search');
var proxyquire = require('proxyquire').noCallThru(); const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {}; module.exports.tests = {};
@ -13,179 +13,529 @@ module.exports.tests.interface = function(test, common) {
}); });
}; };
// reminder: this is only the api subsection of the full config module.exports.tests.success = function(test, common) {
var fakeDefaultConfig = { test('successful request to search service should set data and meta', (t) => {
indexName: 'pelias' const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => {
return {
body: 'this is the query body',
type: 'this is the query type'
};
}; };
// functionally test controller (backend success) // request timeout messages willl be written here
module.exports.tests.functional_success = function(test, common) { const infoMesssages = [];
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search', {
'../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'
});
// expected geojson features for 'client/suggest/ok/1' fixture const docs = [{}, {}];
var expected = [{ const meta = { key: 'value' };
type: 'Feature',
geometry: { callback(undefined, docs, meta);
type: 'Point', },
coordinates: [-50.5, 100.1] 'pelias-logger': {
get: (service) => {
t.equal(service, 'api');
return {
info: (msg) => {
infoMesssages.push(msg);
}, },
properties: { debug: () => {}
id: 'myid1', };
layer: 'mytype1', }
text: 'test name1, city1, state1' }
} })(config, esclient, query);
}, {
type: 'Feature', const req = { clean: { }, errors: [], warnings: [] };
geometry: { const res = {};
type: 'Point',
coordinates: [-51.5, 100.2] const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res.data, [{}, {}]);
t.deepEquals(res.meta, { key: 'value', query_type: 'this is the query type' });
t.ok(infoMesssages.find((msg) => {
return msg === '[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 config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => {
return {
body: 'this is the query body',
type: 'this is the query type'
};
};
// request timeout messages willl be written here
const infoMesssages = [];
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search', {
'../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 = [{}, {}];
callback(undefined, docs, undefined);
}, },
properties: { 'pelias-logger': {
id: 'myid2', get: (service) => {
layer: 'mytype2', t.equal(service, 'api');
text: 'test name2, city2, state2' return {
} info: (msg) => {
}]; infoMesssages.push(msg);
var expectedMeta = {
scores: [10, 20],
query_type: 'mock'
};
var expectedData = [
{
_id: 'myid1',
_score: 10,
_type: 'mytype1',
_matched_queries: ['query 1', 'query 2'],
parent: {
country: ['country1'],
region: ['state1'],
county: ['city1']
}, },
center_point: { lat: 100.1, lon: -50.5 }, debug: () => {}
name: { default: 'test name1' }, };
value: 1 }
}
})(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res.data, [{}, {}]);
t.deepEquals(res.meta, { query_type: 'this is the query type' });
t.ok(infoMesssages.find((msg) => {
return msg === '[controller:search] [queryType:this is the query type] [es_result_count:2]';
}));
t.end();
};
controller(req, res, next);
});
test('undefined docs should log 0 results', (t) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => {
return {
body: 'this is the query body',
type: 'this is the query type'
};
};
// request timeout messages willl be written here
const infoMesssages = [];
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search', {
'../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 meta = { key: 'value' };
callback(undefined, undefined, meta);
}, },
{ 'pelias-logger': {
_id: 'myid2', get: (service) => {
_score: 20, t.equal(service, 'api');
_type: 'mytype2', return {
_matched_queries: ['query 3'], info: (msg) => {
parent: { infoMesssages.push(msg);
country: ['country2'],
region: ['state2'],
county: ['city2']
}, },
center_point: { lat: 100.2, lon: -51.5 }, debug: () => {}
name: { default: 'test name2' }, };
value: 2 }
} }
]; })(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.equals(res.data, undefined);
t.deepEquals(res.meta, { key: 'value', query_type: 'this is the query type' });
t.ok(infoMesssages.find((msg) => {
return msg === '[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) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => {
return {
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'
};
// request timeout messages willl be written here
const infoMesssages = [];
test('functional success', function (t) { // a controller that validates the esclient and cmd that was passed to the search service
var backend = mockBackend('client/search/ok/1', function (cmd) { const controller = proxyquire('../../../controller/search', {
'../service/search': (esclient, cmd, callback) => {
t.equal(esclient, 'this is the esclient');
t.deepEqual(cmd, { t.deepEqual(cmd, {
body: {a: 'b'}, index: 'indexName value',
index: 'pelias', searchType: 'dfs_query_then_fetch',
searchType: 'dfs_query_then_fetch' body: 'this is the query body'
}, 'correct backend command'); });
});
var controller = setup(fakeDefaultConfig, backend, mockQuery()); if (searchServiceCallCount < 2) {
var res = { // note that the searchService got called
status: function (code) { searchServiceCallCount++;
t.equal(code, 200, 'status set'); callback(timeoutError);
return res; } else {
const docs = [{}, {}];
const meta = { key: 'value' };
callback(undefined, docs, meta);
}
}, },
json: function (json) { 'pelias-logger': {
t.equal(typeof json, 'object', 'returns json'); get: (service) => {
t.equal(typeof json.date, 'number', 'date set'); t.equal(service, 'api');
t.equal(json.type, 'FeatureCollection', 'valid geojson'); return {
t.true(Array.isArray(json.features), 'features is array'); info: (msg) => {
t.deepEqual(json.features, expected, 'values correctly mapped'); infoMesssages.push(msg);
} },
}; debug: () => {}
var req = { clean: { a: 'b' }, errors: [], warnings: [] }; };
var next = function next() { }
t.equal(req.errors.length, 0, 'next was called without error'); }
t.deepEqual(res.meta, expectedMeta, 'meta data was set'); })(config, esclient, query);
t.deepEqual(res.data, expectedData, 'data was set');
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.deepEqual(req, {
clean: {},
errors: [],
warnings: []
});
t.deepEquals(res.data, [{}, {}]);
t.deepEquals(res.meta, { key: 'value', query_type: 'this is the query type' });
t.ok(infoMesssages.find((msg) => {
return msg === '[controller:search] [queryType:this is the query type] [es_result_count:2]';
}));
t.ok(infoMesssages.find((msg) => {
return msg === 'succeeded on retry 2';
}));
t.end(); t.end();
}; };
controller(req, res, next); controller(req, res, next);
}); });
test('functional success with alternate index name', function(t) {
var fakeCustomizedConfig = {
indexName: 'alternateindexname'
}; };
var backend = mockBackend('client/search/ok/1', function (cmd) { module.exports.tests.timeout = function(test, common) {
test('default # of request timeout retries should be 3', (t) => {
const config = {
indexName: 'indexName value'
};
const esclient = 'this is the esclient';
const query = () => {
return { body: 'this is the query body' };
};
let searchServiceCallCount = 0;
const timeoutError = {
status: 408,
displayName: 'RequestTimeout',
message: 'Request Timeout after 17ms'
};
// request timeout messages willl be written here
const infoMesssages = [];
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search', {
'../service/search': (esclient, cmd, callback) => {
t.equal(esclient, 'this is the esclient');
t.deepEqual(cmd, { t.deepEqual(cmd, {
body: {a: 'b'}, index: 'indexName value',
index: 'alternateindexname', searchType: 'dfs_query_then_fetch',
searchType: 'dfs_query_then_fetch' body: 'this is the query body'
}, 'correct backend command');
}); });
var controller = setup(fakeCustomizedConfig, backend, mockQuery());
var res = { // not that the searchService got called
status: function (code) { searchServiceCallCount++;
t.equal(code, 200, 'status set');
return res; callback(timeoutError);
},
'pelias-logger': {
get: (service) => {
t.equal(service, 'api');
return {
info: (msg) => {
infoMesssages.push(msg);
},
debug: () => {}
};
}
} }
})(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 3+1);
t.ok(infoMesssages.indexOf('request timed out on attempt 1, retrying') !== -1);
t.ok(infoMesssages.indexOf('request timed out on attempt 2, retrying') !== -1);
t.ok(infoMesssages.indexOf('request timed out on attempt 3, retrying') !== -1);
t.deepEqual(req, {
clean: {},
errors: [timeoutError.message],
warnings: []
});
t.deepEqual(res, {});
t.end();
}; };
var req = { clean: { a: 'b' }, errors: [], warnings: [] };
var next = function next() { controller(req, res, next);
t.equal(req.errors.length, 0, 'next was called without error');
});
test('explicit apiConfig.requestRetries should retry that many times', (t) => {
const config = {
indexName: 'indexName value',
requestRetries: 17
};
const esclient = 'this is the esclient';
const query = () => {
return { };
};
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', {
'../service/search': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(timeoutError);
}
})(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 17+1);
t.end(); t.end();
}; };
controller(req, res, next); controller(req, res, next);
}); });
test('only status code 408 should be considered a retryable request', (t) => {
const config = {
indexName: 'indexName value',
requestRetries: 17
}; };
const esclient = 'this is the esclient';
const query = () => {
return { };
};
let searchServiceCallCount = 0;
const nonTimeoutError = {
status: 500,
displayName: 'InternalServerError',
message: 'an internal server error occurred'
};
// a controller that validates the esclient and cmd that was passed to the search service
const controller = proxyquire('../../../controller/search', {
'../service/search': (esclient, cmd, callback) => {
// not that the searchService got called
searchServiceCallCount++;
callback(nonTimeoutError);
}
})(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
// functionally test controller (backend failure) const next = () => {
module.exports.tests.functional_failure = function(test, common) { t.equal(searchServiceCallCount, 1);
test('functional failure', function(t) { t.deepEqual(req, {
var backend = mockBackend( 'client/search/fail/1', function( cmd ){ clean: {},
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias', searchType: 'dfs_query_then_fetch' }, 'correct backend command'); errors: [nonTimeoutError.message],
warnings: []
}); });
var controller = setup( fakeDefaultConfig, backend, mockQuery() );
var req = { clean: { a: 'b' }, errors: [], warnings: [] };
var next = function(){
t.equal(req.errors[0],'a backend error occurred');
t.end(); t.end();
}; };
controller(req, undefined, next );
controller(req, res, next);
}); });
test('string error should not retry and be logged as-is', (t) => {
const config = {
indexName: 'indexName value',
requestRetries: 17
};
const esclient = 'this is the esclient';
const query = () => {
return { };
}; };
module.exports.tests.timeout = function(test, common) { let searchServiceCallCount = 0;
test('timeout', function(t) {
var backend = mockBackend( 'client/search/timeout/1', function( cmd ){ const stringTypeError = 'this is an error string';
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias', searchType: 'dfs_query_then_fetch' }, 'correct backend command');
}); // a controller that validates the esclient and cmd that was passed to the search service
var controller = setup( fakeDefaultConfig, backend, mockQuery() ); const controller = proxyquire('../../../controller/search', {
var req = { clean: { a: 'b' }, errors: [], warnings: [] }; '../service/search': (esclient, cmd, callback) => {
var next = function(){ // not that the searchService got called
t.equal(req.errors[0],'Request Timeout after 5000ms'); searchServiceCallCount++;
callback(stringTypeError);
}
})(config, esclient, query);
const req = { clean: { }, errors: [], warnings: [] };
const res = {};
const next = () => {
t.equal(searchServiceCallCount, 1);
t.deepEqual(req, {
clean: {},
errors: [stringTypeError],
warnings: []
});
t.end(); t.end();
}; };
controller(req, undefined, next );
controller(req, res, next);
}); });
};
module.exports.tests.existing_errors = function(test, common) {
test('req with errors should not call esclient or query', function(t) {
const esclient = () => {
throw new Error('esclient should not have been called');
};
const query = () => {
throw new Error('query should not have been called');
};
const controller = setup( {}, esclient, query );
// the existence of `errors` means that a sanitizer detected an error,
// so don't call the esclient
const req = {
errors: ['error']
};
const res = { };
t.doesNotThrow(() => {
controller(req, res, () => {});
});
t.end();
});
}; };
module.exports.tests.existing_results = function(test, common) { module.exports.tests.existing_results = function(test, common) {
test('res with existing data should not call backend', function(t) { test('res with existing data should not call esclient or query', function(t) {
var backend = function() { const esclient = () => {
throw new Error('backend should not have been called'); throw new Error('esclient should not have been called');
}; };
var controller = setup( fakeDefaultConfig, backend, mockQuery() ); const query = () => {
throw new Error('query should not have been called');
};
const controller = setup( {}, esclient, query );
var req = { }; const req = { };
// the existence of `data` means that there are already results so // the existence of `data` means that there are already results so
// don't call the backend/query // don't call esclient or query
var res = { data: [{}] }; const res = { data: [{}] };
var next = function() { const next = function() {
t.deepEqual(res, {data: [{}]}); t.deepEqual(res, {data: [{}]});
t.end(); t.end();
}; };
@ -198,18 +548,20 @@ module.exports.tests.existing_results = function(test, common) {
module.exports.tests.undefined_query = function(test, common) { module.exports.tests.undefined_query = function(test, common) {
test('query returning undefined should not call service', function(t) { test('query returning undefined should not call service', function(t) {
// a function that returns undefined // a function that returns undefined
var query = function () { return; }; const query = () => {
return undefined;
};
var search_service_was_called = false; let search_service_was_called = false;
var controller = proxyquire('../../../controller/search', { const controller = proxyquire('../../../controller/search', {
'../service/search': function() { '../service/search': function() {
search_service_was_called = true; search_service_was_called = true;
throw new Error('search service should not have been called'); throw new Error('search service should not have been called');
} }
})(undefined, undefined, query); })(undefined, undefined, query);
var next = function() { const next = () => {
t.notOk(search_service_was_called, 'should have returned before search service was called'); t.notOk(search_service_was_called, 'should have returned before search service was called');
t.end(); t.end();
}; };
@ -225,7 +577,7 @@ module.exports.all = function (tape, common) {
return tape('GET /search ' + name, testFunction); return tape('GET /search ' + name, testFunction);
} }
for( var testCase in module.exports.tests ){ for( const testCase in module.exports.tests ){
module.exports.tests[testCase](test, common); module.exports.tests[testCase](test, common);
} }
}; };

16
test/unit/middleware/parseBBox.js

@ -1,5 +1,7 @@
var parseBBox = require('../../../middleware/parseBBox')(); var parseBBox = require('../../../middleware/parseBBox')();
const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.computeDistance = function(test, common) { module.exports.tests.computeDistance = function(test, common) {
@ -46,6 +48,20 @@ module.exports.tests.computeDistance = function(test, common) {
] ]
}; };
const parseBBox = proxyquire('../../../middleware/parseBBox', {
'pelias-logger': {
get: () => {
return {
error: (msg1, msg2) => {
t.equals(msg1, 'Invalid bounding_box json string:');
t.deepEquals(msg2, { bounding_box: 'garbage json' });
}
};
}
}
})();
parseBBox({}, res, function () { parseBBox({}, res, function () {
t.deepEquals(res, expected, 'correct bounding_box'); t.deepEquals(res, expected, 'correct bounding_box');
t.end(); t.end();

251
test/unit/mock/alpha3.json

@ -1,251 +0,0 @@
{
"ABW": "Aruba",
"AFG": "Afghanistan",
"AGO": "Angola",
"AIA": "Anguilla",
"ALA": "Ă…land Islands",
"ALB": "Albania",
"AND": "Andorra",
"ARE": "United Arab Emirates",
"ARG": "Argentina",
"ARM": "Armenia",
"ASM": "American Samoa",
"ATA": "Antarctica",
"ATF": "French Southern Territories",
"ATG": "Antigua and Barbuda",
"AUS": "Australia",
"AUT": "Austria",
"AZE": "Azerbaijan",
"BDI": "Burundi",
"BEL": "Belgium",
"BEN": "Benin",
"BES": "Bonaire, Sint Eustatius and Saba",
"BFA": "Burkina Faso",
"BGD": "Bangladesh",
"BGR": "Bulgaria",
"BHR": "Bahrain",
"BHS": "Bahamas",
"BIH": "Bosnia and Herzegovina",
"BLM": "Saint Barthélemy",
"BLR": "Belarus",
"BLZ": "Belize",
"BMU": "Bermuda",
"BOL": "Bolivia, Plurinational State of",
"BRA": "Brazil",
"BRB": "Barbados",
"BRN": "Brunei Darussalam",
"BTN": "Bhutan",
"BVT": "Bouvet Island",
"BWA": "Botswana",
"CAF": "Central African Republic",
"CAN": "Canada",
"CCK": "Cocos (Keeling) Islands",
"CHE": "Switzerland",
"CHL": "Chile",
"CHN": "China",
"CIV": "CĂ´te d'Ivoire",
"CMR": "Cameroon",
"COD": "Congo, the Democratic Republic of the",
"COG": "Congo",
"COK": "Cook Islands",
"COL": "Colombia",
"COM": "Comoros",
"CPV": "Cabo Verde",
"CRI": "Costa Rica",
"CUB": "Cuba",
"CUW": "Curaçao",
"CXR": "Christmas Island",
"CYM": "Cayman Islands",
"CYP": "Cyprus",
"CZE": "Czech Republic",
"DEU": "Germany",
"DJI": "Djibouti",
"DMA": "Dominica",
"DNK": "Denmark",
"DOM": "Dominican Republic",
"DZA": "Algeria",
"ECU": "Ecuador",
"EGY": "Egypt",
"ERI": "Eritrea",
"ESH": "Western Sahara",
"ESP": "Spain",
"EST": "Estonia",
"ETH": "Ethiopia",
"FIN": "Finland",
"FJI": "Fiji",
"FLK": "Falkland Islands (Malvinas)",
"FRA": "France",
"FRO": "Faroe Islands",
"FSM": "Micronesia, Federated States of",
"GAB": "Gabon",
"GBR": "United Kingdom",
"GEO": "Georgia",
"GGY": "Guernsey",
"GHA": "Ghana",
"GIB": "Gibraltar",
"GIN": "Guinea",
"GLP": "Guadeloupe",
"GMB": "Gambia",
"GNB": "Guinea-Bissau",
"GNQ": "Equatorial Guinea",
"GRC": "Greece",
"GRD": "Grenada",
"GRL": "Greenland",
"GTM": "Guatemala",
"GUF": "French Guiana",
"GUM": "Guam",
"GUY": "Guyana",
"HKG": "Hong Kong",
"HMD": "Heard Island and McDonald Islands",
"HND": "Honduras",
"HRV": "Croatia",
"HTI": "Haiti",
"HUN": "Hungary",
"IDN": "Indonesia",
"IMN": "Isle of Man",
"IND": "India",
"IOT": "British Indian Ocean Territory",
"IRL": "Ireland",
"IRN": "Iran, Islamic Republic of",
"IRQ": "Iraq",
"ISL": "Iceland",
"ISR": "Israel",
"ITA": "Italy",
"JAM": "Jamaica",
"JEY": "Jersey",
"JOR": "Jordan",
"JPN": "Japan",
"KAZ": "Kazakhstan",
"KEN": "Kenya",
"KGZ": "Kyrgyzstan",
"KHM": "Cambodia",
"KIR": "Kiribati",
"KNA": "Saint Kitts and Nevis",
"KOR": "Korea, Republic of",
"KWT": "Kuwait",
"LAO": "Lao People's Democratic Republic",
"LBN": "Lebanon",
"LBR": "Liberia",
"LBY": "Libya",
"LCA": "Saint Lucia",
"LIE": "Liechtenstein",
"LKA": "Sri Lanka",
"LSO": "Lesotho",
"LTU": "Lithuania",
"LUX": "Luxembourg",
"LVA": "Latvia",
"MAC": "Macao",
"MAF": "Saint Martin (French part)",
"MAR": "Morocco",
"MCO": "Monaco",
"MDA": "Moldova, Republic of",
"MDG": "Madagascar",
"MDV": "Maldives",
"MEX": "Mexico",
"MHL": "Marshall Islands",
"MKD": "Macedonia, the former Yugoslav Republic of",
"MLI": "Mali",
"MLT": "Malta",
"MMR": "Myanmar",
"MNE": "Montenegro",
"MNG": "Mongolia",
"MNP": "Northern Mariana Islands",
"MOZ": "Mozambique",
"MRT": "Mauritania",
"MSR": "Montserrat",
"MTQ": "Martinique",
"MUS": "Mauritius",
"MWI": "Malawi",
"MYS": "Malaysia",
"MYT": "Mayotte",
"NAM": "Namibia",
"NCL": "New Caledonia",
"NER": "Niger",
"NFK": "Norfolk Island",
"NGA": "Nigeria",
"NIC": "Nicaragua",
"NIU": "Niue",
"NLD": "Netherlands",
"NOR": "Norway",
"NPL": "Nepal",
"NRU": "Nauru",
"NZL": "New Zealand",
"OMN": "Oman",
"PAK": "Pakistan",
"PAN": "Panama",
"PCN": "Pitcairn",
"PER": "Peru",
"PHL": "Philippines",
"PLW": "Palau",
"PNG": "Papua New Guinea",
"POL": "Poland",
"PRI": "Puerto Rico",
"PRK": "Korea, Democratic People's Republic of",
"PRT": "Portugal",
"PRY": "Paraguay",
"PSE": "Palestine, State of",
"PYF": "French Polynesia",
"QAT": "Qatar",
"REU": "RĂ©union",
"ROU": "Romania",
"RUS": "Russian Federation",
"RWA": "Rwanda",
"SAU": "Saudi Arabia",
"SDN": "Sudan",
"SEN": "Senegal",
"SGP": "Singapore",
"SGS": "South Georgia and the South Sandwich Islands",
"SHN": "Saint Helena, Ascension and Tristan da Cunha",
"SJM": "Svalbard and Jan Mayen",
"SLB": "Solomon Islands",
"SLE": "Sierra Leone",
"SLV": "El Salvador",
"SMR": "San Marino",
"SOM": "Somalia",
"SPM": "Saint Pierre and Miquelon",
"SRB": "Serbia",
"SSD": "South Sudan",
"STP": "Sao Tome and Principe",
"SUR": "Suriname",
"SVK": "Slovakia",
"SVN": "Slovenia",
"SWE": "Sweden",
"SWZ": "Swaziland",
"SXM": "Sint Maarten (Dutch part)",
"SYC": "Seychelles",
"SYR": "Syrian Arab Republic",
"TCA": "Turks and Caicos Islands",
"TCD": "Chad",
"TGO": "Togo",
"THA": "Thailand",
"TJK": "Tajikistan",
"TKL": "Tokelau",
"TKM": "Turkmenistan",
"TLS": "Timor-Leste",
"TON": "Tonga",
"TTO": "Trinidad and Tobago",
"TUN": "Tunisia",
"TUR": "Turkey",
"TUV": "Tuvalu",
"TWN": "Taiwan, Province of China",
"TZA": "Tanzania, United Republic of",
"UGA": "Uganda",
"UKR": "Ukraine",
"UMI": "United States Minor Outlying Islands",
"URY": "Uruguay",
"USA": "United States",
"UZB": "Uzbekistan",
"VAT": "Holy See (Vatican City State)",
"VCT": "Saint Vincent and the Grenadines",
"VEN": "Venezuela, Bolivarian Republic of",
"VGB": "Virgin Islands, British",
"VIR": "Virgin Islands, U.S.",
"VNM": "Viet Nam",
"VUT": "Vanuatu",
"WLF": "Wallis and Futuna",
"WSM": "Samoa",
"YEM": "Yemen",
"ZAF": "South Africa",
"ZMB": "Zambia",
"ZWE": "Zimbabwe"
}

20
test/unit/mock/backend.js

@ -1,11 +1,5 @@
var responses = {}; var responses = {};
responses['client/suggest/ok/1'] = function( cmd, cb ){
return cb( undefined, suggestEnvelope([ { score: 1, text: 'mocktype:mockid1' } ], [ { score: 2, text: 'mocktype:mockid2' } ]) );
};
responses['client/suggest/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' );
};
responses['client/search/ok/1'] = function( cmd, cb ){ responses['client/search/ok/1'] = function( cmd, cb ){
return cb( undefined, searchEnvelope([{ return cb( undefined, searchEnvelope([{
_id: 'myid1', _id: 'myid1',
@ -32,15 +26,7 @@ responses['client/search/ok/1'] = function( cmd, cb ){
}])); }]));
}; };
responses['client/search/fail/1'] = function( cmd, cb ){ responses['client/search/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' ); return cb( 'an elasticsearch error occurred' );
};
responses['client/search/timeout/1'] = function( cmd, cb) {
// timeout errors are objects
return cb({
status: 408,
message: 'Request Timeout after 5000ms'
});
}; };
responses['client/mget/ok/1'] = function( cmd, cb ){ responses['client/mget/ok/1'] = function( cmd, cb ){
@ -73,7 +59,6 @@ responses['client/mget/fail/1'] = responses['client/search/fail/1'];
function setup( key, cmdCb ){ function setup( key, cmdCb ){
function backend( a, b ){ function backend( a, b ){
return { return {
client: {
mget: function( cmd, cb ){ mget: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); } if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key.indexOf('mget') === -1 ? 'client/mget/ok/1' : key].apply( this, arguments ); return responses[key.indexOf('mget') === -1 ? 'client/mget/ok/1' : key].apply( this, arguments );
@ -86,10 +71,9 @@ function setup( key, cmdCb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); } if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments ); return responses[key].apply( this, arguments );
} }
}
}; };
} }
return backend; return backend();
} }
function mgetEnvelope( options ){ function mgetEnvelope( options ){

13
test/unit/mock/query.js

@ -1,13 +0,0 @@
function setup(){
return query;
}
function query( clean ){
return {
type: 'mock',
body: clean
};
}
module.exports = setup;

3
test/unit/run.js

@ -10,6 +10,7 @@ var common = {
var tests = [ var tests = [
require('./app'), require('./app'),
require('./schema'),
require('./controller/index'), require('./controller/index'),
require('./controller/place'), require('./controller/place'),
require('./controller/search'), require('./controller/search'),
@ -62,8 +63,6 @@ var tests = [
require('./sanitizer/_deprecate_quattroshapes'), require('./sanitizer/_deprecate_quattroshapes'),
require('./sanitizer/_categories'), require('./sanitizer/_categories'),
require('./sanitizer/nearby'), require('./sanitizer/nearby'),
require('./src/backend'),
require('./src/configValidation'),
require('./sanitizer/autocomplete'), require('./sanitizer/autocomplete'),
require('./sanitizer/structured_geocoding'), require('./sanitizer/structured_geocoding'),
require('./sanitizer/place'), require('./sanitizer/place'),

64
test/unit/sanitizer/search_fallback.js

@ -107,6 +107,70 @@ module.exports.tests.sanitize = function(test, common) {
}); });
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) { module.exports.all = function (tape, common) {

226
test/unit/src/configValidation.js → test/unit/schema.js

@ -1,11 +1,20 @@
'use strict'; 'use strict';
const configValidation = require('../../../src/configValidation'); const Joi = require('joi');
const schema = require('../../schema');
function validate(config) {
Joi.validate(config, schema, (err, value) => {
if (err) {
throw new Error(err.details[0].message);
}
});
}
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.completely_valid = function(test, common) { module.exports.tests.completely_valid = (test, common) => {
test('all valid configuration elements should not throw error', function(t) { test('all valid configuration elements should not throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -16,22 +25,20 @@ module.exports.tests.completely_valid = function(test, common) {
relativeScores: true, relativeScores: true,
localization: { localization: {
flipNumberAndStreetCountries: ['ABC', 'DEF'] flipNumberAndStreetCountries: ['ABC', 'DEF']
} },
requestRetries: 19
}, },
esclient: { esclient: {
requestTimeout: 17 requestTimeout: 17
} }
}; };
t.doesNotThrow(function() { t.doesNotThrow(validate.bind(config));
configValidation.validate(config);
});
t.end(); t.end();
}); });
test('basic valid configurtaion should not throw error', function(t) { test('basic valid configuration should not throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -43,30 +50,25 @@ module.exports.tests.completely_valid = function(test, common) {
} }
}; };
t.doesNotThrow(function() { t.doesNotThrow(validate.bind(config));
configValidation.validate(config);
});
t.end(); t.end();
}); });
}; };
module.exports.tests.api_validation = function(test, common) { module.exports.tests.api_validation = (test, common) => {
test('config without api should throw error', function(t) { test('config without api should throw error', (t) => {
var config = { var config = {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"api" is required/, 'api should exist');
configValidation.validate(config);
}, /"api" is required/, 'api should exist');
t.end(); t.end();
}); });
test('config without unknown field in api should not throw error', function(t) { test('config without unknown field in api should not throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -77,14 +79,12 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.doesNotThrow(function() { t.doesNotThrow(validate.bind(null, config), 'unknown properties should be allowed');
configValidation.validate(config);
}, 'unknown properties should be allowed');
t.end(); t.end();
}); });
test('non-string api.version should throw error', function(t) { test('non-string api.version should throw error', (t) => {
[null, 17, {}, [], true].forEach((value) => { [null, 17, {}, [], true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -95,16 +95,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"version" must be a string/);
configValidation.validate(config);
}, /"version" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-string api.indexName should throw error', function(t) { test('non-string api.indexName should throw error', (t) => {
[null, 17, {}, [], true].forEach((value) => { [null, 17, {}, [], true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -115,16 +114,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"indexName" must be a string/);
configValidation.validate(config);
}, /"indexName" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-string api.host should throw error', function(t) { test('non-string api.host should throw error', (t) => {
[null, 17, {}, [], true].forEach((value) => { [null, 17, {}, [], true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -135,16 +133,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"host" must be a string/);
configValidation.validate(config);
}, /"host" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-string api.legacyUrl should throw error', function(t) { test('non-string api.legacyUrl should throw error', (t) => {
[null, 17, {}, [], true].forEach((value) => { [null, 17, {}, [], true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -156,16 +153,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"legacyUrl" must be a string/);
configValidation.validate(config);
}, /"legacyUrl" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-string api.accessLog should throw error', function(t) { test('non-string api.accessLog should throw error', (t) => {
[null, 17, {}, [], true].forEach((value) => { [null, 17, {}, [], true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -177,16 +173,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"accessLog" must be a string/);
configValidation.validate(config);
}, /"accessLog" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-boolean api.relativeScores should throw error', function(t) { test('non-boolean api.relativeScores should throw error', (t) => {
[null, 17, {}, [], 'string'].forEach((value) => { [null, 17, {}, [], 'string'].forEach((value) => {
var config = { var config = {
api: { api: {
@ -198,16 +193,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"relativeScores" must be a boolean/);
configValidation.validate(config);
}, /"relativeScores" must be a boolean/);
}); });
t.end(); t.end();
}); });
test('non-object api.localization should throw error', function(t) { test('non-object api.localization should throw error', (t) => {
[null, 17, false, [], 'string'].forEach((value) => { [null, 17, false, [], 'string'].forEach((value) => {
var config = { var config = {
api: { api: {
@ -219,16 +213,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"localization" must be an object/);
configValidation.validate(config);
}, /"localization" must be an object/);
}); });
t.end(); t.end();
}); });
test('unknown properties in api.localization should throw error', function(t) { test('unknown properties in api.localization should throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -241,15 +234,13 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"unknown_property" is not allowed/);
configValidation.validate(config);
}, /"unknown_property" is not allowed/);
t.end(); t.end();
}); });
test('non-array api.localization.flipNumberAndStreetCountries should throw error', function(t) { test('non-array api.localization.flipNumberAndStreetCountries should throw error', (t) => {
[null, 17, {}, false, 'string'].forEach((value) => { [null, 17, {}, false, 'string'].forEach((value) => {
var config = { var config = {
api: { api: {
@ -263,16 +254,17 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"flipNumberAndStreetCountries" must be an array/); /"flipNumberAndStreetCountries" must be an array/);
}); });
t.end(); t.end();
}); });
test('non-string api.localization.flipNumberAndStreetCountries elements should throw error', function(t) { test('non-string api.localization.flipNumberAndStreetCountries elements should throw error', (t) => {
[null, 17, {}, false, []].forEach((value) => { [null, 17, {}, false, []].forEach((value) => {
var config = { var config = {
api: { api: {
@ -286,16 +278,15 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /"0" must be a string/);
configValidation.validate(config);
}, /"0" must be a string/);
}); });
t.end(); t.end();
}); });
test('non-3-char api.localization.flipNumberAndStreetCountries elements should throw error', function(t) { test('non-3-char api.localization.flipNumberAndStreetCountries elements should throw error', (t) => {
['AB', 'ABCD'].forEach((value) => { ['AB', 'ABCD'].forEach((value) => {
var config = { var config = {
api: { api: {
@ -309,19 +300,77 @@ module.exports.tests.api_validation = function(test, common) {
esclient: {} esclient: {}
}; };
t.throws(function() { t.throws(validate.bind(null, config), /fails to match the required pattern/);
configValidation.validate(config);
}, /fails to match the required pattern/);
}); });
t.end(); t.end();
}); });
test('config with non-number api.requestRetries should throw error', (t) => {
[null, 'string', {}, [], false].forEach((value) => {
var config = {
api: {
version: 'version value',
indexName: 'index name value',
host: 'host value',
requestRetries: value
},
esclient: {}
}; };
module.exports.tests.esclient_validation = function(test, common) { t.throws(
test('config without esclient should throw error', function(t) { validate.bind(null, config),
/"requestRetries" must be a number/, 'api.requestRetries should be a number');
});
t.end();
});
test('config with non-integer api.requestRetries should throw error', (t) => {
var config = {
api: {
version: 'version value',
indexName: 'index name value',
host: 'host value',
requestRetries: 17.3
},
esclient: {}
};
t.throws(
validate.bind(null, config),
/"requestRetries" must be an integer/, 'api.requestRetries should be an integer');
t.end();
});
test('config with negative api.requestRetries should throw error', (t) => {
var config = {
api: {
version: 'version value',
indexName: 'index name value',
host: 'host value',
requestRetries: -1
},
esclient: {}
};
t.throws(
validate.bind(null, config),
/"requestRetries" must be larger than or equal to 0/, 'api.requestRetries must be positive');
t.end();
});
};
module.exports.tests.esclient_validation = (test, common) => {
test('config without esclient should throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -330,14 +379,14 @@ module.exports.tests.esclient_validation = function(test, common) {
} }
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"esclient" is required/, 'esclient should exist'); /"esclient" is required/, 'esclient should exist');
t.end(); t.end();
}); });
test('config with non-object esclient should throw error', function(t) { test('config with non-object esclient should throw error', (t) => {
[null, 17, [], 'string', true].forEach((value) => { [null, 17, [], 'string', true].forEach((value) => {
var config = { var config = {
api: { api: {
@ -348,9 +397,9 @@ module.exports.tests.esclient_validation = function(test, common) {
esclient: value esclient: value
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"esclient" must be an object/, 'esclient should be an object'); /"esclient" must be an object/, 'esclient should be an object');
}); });
@ -358,7 +407,7 @@ module.exports.tests.esclient_validation = function(test, common) {
}); });
test('config with non-integer esclient.requestTimeout should throw error', function(t) { test('config with non-number esclient.requestTimeout should throw error', (t) => {
[null, 'string', {}, [], false].forEach((value) => { [null, 'string', {}, [], false].forEach((value) => {
var config = { var config = {
api: { api: {
@ -371,16 +420,17 @@ module.exports.tests.esclient_validation = function(test, common) {
} }
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"requestTimeout" must be a number/, 'esclient.requestTimeout should be a number'); /"requestTimeout" must be a number/, 'esclient.requestTimeout should be a number');
}); });
t.end(); t.end();
}); });
test('config with non-integer esclient.requestTimeout should throw error', function(t) { test('config with non-integer esclient.requestTimeout should throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -392,15 +442,15 @@ module.exports.tests.esclient_validation = function(test, common) {
} }
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"requestTimeout" must be an integer/, 'esclient.requestTimeout should be an integer'); /"requestTimeout" must be an integer/, 'esclient.requestTimeout should be an integer');
t.end(); t.end();
}); });
test('config with negative esclient.requestTimeout should throw error', function(t) { test('config with negative esclient.requestTimeout should throw error', (t) => {
var config = { var config = {
api: { api: {
version: 'version value', version: 'version value',
@ -412,9 +462,9 @@ module.exports.tests.esclient_validation = function(test, common) {
} }
}; };
t.throws(function() { t.throws(
configValidation.validate(config); validate.bind(null, config),
}, /"requestTimeout" must be larger than or equal to 0/, 'esclient.requestTimeout must be positive'); /"requestTimeout" must be larger than or equal to 0/, 'esclient.requestTimeout must be positive');
t.end(); t.end();
@ -422,7 +472,7 @@ module.exports.tests.esclient_validation = function(test, common) {
}; };
module.exports.all = function (tape, common) { module.exports.all = (tape, common) => {
function test(name, testFunction) { function test(name, testFunction) {
return tape('configValidation: ' + name, testFunction); return tape('configValidation: ' + name, testFunction);

35
test/unit/service/mget.js

@ -1,12 +1,23 @@
var setup = require('../../../service/mget'), var service = require('../../../service/mget'),
mockBackend = require('../mock/backend'); mockBackend = require('../mock/backend');
const proxyquire = require('proxyquire').noCallThru();
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.interface = function(test, common) { module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) { test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function'); var service = proxyquire('../../../service/mget', {
'pelias-logger': {
get: (section) => {
t.equal(section, 'api');
}
}
});
t.equal(typeof service, 'function', 'service is a function');
t.end(); t.end();
}); });
}; };
@ -35,7 +46,7 @@ module.exports.tests.functional_success = function(test, common) {
var backend = mockBackend( 'client/mget/ok/1', function( cmd ){ var backend = mockBackend( 'client/mget/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command'); t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command');
}); });
setup( backend, [ { _id: 123, _index: 'pelias', _type: 'a' } ], function(err, data) { service( backend, [ { _id: 123, _index: 'pelias', _type: 'a' } ], function(err, data) {
t.true(Array.isArray(data), 'returns an array'); t.true(Array.isArray(data), 'returns an array');
data.forEach(function(d) { data.forEach(function(d) {
t.true(typeof d === 'object', 'valid object'); t.true(typeof d === 'object', 'valid object');
@ -62,8 +73,22 @@ module.exports.tests.functional_failure = function(test, common) {
t.notDeepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'incorrect backend command'); t.notDeepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'incorrect backend command');
}); });
invalid_queries.forEach(function(query) { invalid_queries.forEach(function(query) {
setup( backend, [ query ], function(err, data) { // mock out pelias-logger so we can assert what's being logged
t.equal(err, 'a backend error occurred','error passed to errorHandler'); var service = proxyquire('../../../service/mget', {
'pelias-logger': {
get: () => {
return {
error: (msg) => {
t.equal(msg, 'elasticsearch error an elasticsearch error occurred');
}
};
}
}
});
service( backend, [ query ], function(err, data) {
t.equal(err, 'an elasticsearch error occurred','error passed to errorHandler');
t.equal(data, undefined, 'data is undefined'); t.equal(data, undefined, 'data is undefined');
}); });
}); });

35
test/unit/service/search.js

@ -1,14 +1,25 @@
var setup = require('../../../service/search'), var service = require('../../../service/search'),
mockBackend = require('../mock/backend'); mockBackend = require('../mock/backend');
const proxyquire = require('proxyquire').noCallThru();
var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' }; var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' };
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.interface = function(test, common) { module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) { test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function'); var service = proxyquire('../../../service/mget', {
'pelias-logger': {
get: (section) => {
t.equal(section, 'api');
}
}
});
t.equal(typeof service, 'function', 'service is a function');
t.end(); t.end();
}); });
}; };
@ -45,7 +56,7 @@ module.exports.tests.functional_success = function(test, common) {
var backend = mockBackend( 'client/search/ok/1', function( cmd ){ var backend = mockBackend( 'client/search/ok/1', function( cmd ){
t.deepEqual(cmd, example_valid_es_query, 'no change to the command'); t.deepEqual(cmd, example_valid_es_query, 'no change to the command');
}); });
setup( backend, example_valid_es_query, function(err, data, meta) { service( backend, example_valid_es_query, function(err, data, meta) {
t.true(Array.isArray(data), 'returns an array'); t.true(Array.isArray(data), 'returns an array');
data.forEach(function(d) { data.forEach(function(d) {
t.true(typeof d === 'object', 'valid object'); t.true(typeof d === 'object', 'valid object');
@ -71,8 +82,22 @@ module.exports.tests.functional_failure = function(test, common) {
t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command'); t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command');
}); });
invalid_queries.forEach(function(query) { invalid_queries.forEach(function(query) {
setup( backend, [ query ], function(err, data) { // mock out pelias-logger so we can assert what's being logged
t.equal(err, 'a backend error occurred','error passed to errorHandler'); var service = proxyquire('../../../service/search', {
'pelias-logger': {
get: () => {
return {
error: (msg) => {
t.equal(msg, 'elasticsearch error an elasticsearch error occurred');
}
};
}
}
});
service( backend, [ query ], function(err, data) {
t.equal(err, 'an elasticsearch error occurred','error passed to errorHandler');
t.equal(data, undefined, 'data is undefined'); t.equal(data, undefined, 'data is undefined');
}); });
}); });

40
test/unit/src/backend.js

@ -1,40 +0,0 @@
var proxyquire = require('proxyquire');
var stubConfig = {
generate: function generate() {
return {
esclient: {
hosts: [
'http://notLocalhost:9200',
'http://anotherHost:9200'
]
}
};
}
};
module.exports.tests = {};
module.exports.tests.config_properly_passed = function(test, common) {
test('Elasticsearch config is properly passed to elasticsearch module', function(t) {
var stubElasticsearchClient = {
Client: function(config) {
t.deepEquals(config.hosts, [ 'http://notLocalhost:9200', 'http://anotherHost:9200' ], 'hosts set correctly' );
t.end();
}
};
proxyquire('../../../src/backend', { 'pelias-config': stubConfig, 'elasticsearch': stubElasticsearchClient } );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE src/backend ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};
Loading…
Cancel
Save