From f14400ebce30cbea4c75c52440876f8d35a4a4f0 Mon Sep 17 00:00:00 2001 From: Stephen Hess Date: Mon, 17 Apr 2017 13:56:44 -0400 Subject: [PATCH] added placeholder service support to API for admin-only queries Adds a controller, service (which could probably be made more generic and share with pointinpolygon), and predicates for determining when placeholder should be called. This also renames `is_pip_service_enabled.js` to `is_service_enabled.js` since it's generic. If placeholder returns a non-empty response then `res.data` is populated, otherwise `res.data` is not populated, allowing fallback to existing production behavior. --- controller/placeholder.js | 73 ++++ .../predicates/is_admin_only_analysis.js | 13 + .../predicates/is_pip_service_enabled.js | 7 - controller/predicates/is_service_enabled.js | 5 + package.json | 4 +- routes/v1.js | 14 +- service/placeholder.js | 62 +++ test/unit/controller/placeholder.js | 378 ++++++++++++++++++ .../predicates/is_admin_only_analysis.js | 77 ++++ ...rvice_enabled.js => is_service_enabled.js} | 12 +- test/unit/run.js | 5 +- test/unit/service/placeholder.js | 193 +++++++++ 12 files changed, 826 insertions(+), 17 deletions(-) create mode 100644 controller/placeholder.js create mode 100644 controller/predicates/is_admin_only_analysis.js delete mode 100644 controller/predicates/is_pip_service_enabled.js create mode 100644 controller/predicates/is_service_enabled.js create mode 100644 service/placeholder.js create mode 100644 test/unit/controller/placeholder.js create mode 100644 test/unit/controller/predicates/is_admin_only_analysis.js rename test/unit/controller/predicates/{is_pip_service_enabled.js => is_service_enabled.js} (58%) create mode 100644 test/unit/service/placeholder.js diff --git a/controller/placeholder.js b/controller/placeholder.js new file mode 100644 index 00000000..57ce4f21 --- /dev/null +++ b/controller/placeholder.js @@ -0,0 +1,73 @@ +'use strict'; + +const _ = require('lodash'); + +const logger = require('pelias-logger').get('api'); +const logging = require( '../helper/logging' ); +const Document = require('pelias-model').Document; + +function synthesizeDocs(result) { + return result.lineage.map((hierarchy) => { + const doc = new Document('whosonfirst', result.placetype, result.id.toString()); + doc.setName('default', result.name); + doc.setCentroid( { lat: result.geom.lat, lon: result.geom.lon } ); + + const parsedBoundingBox = result.geom.bbox.split(',').map(parseFloat); + doc.setBoundingBox({ + upperLeft: { + lat: parsedBoundingBox[3], + lon: parsedBoundingBox[0] + }, + lowerRight: { + lat: parsedBoundingBox[1], + lon: parsedBoundingBox[2] + } + }); + + Object.keys(hierarchy) + .filter(doc.isSupportedParent) + .filter((placetype) => { return !_.isEmpty(_.trim(hierarchy[placetype].name)); } ) + .forEach((placetype) => { + if (hierarchy[placetype].hasOwnProperty('abbr') && placetype === 'country') { + doc.setAlpha3(hierarchy[placetype].abbr); + } + + doc.addParent( + placetype, + hierarchy[placetype].name, + hierarchy[placetype].id.toString(), + hierarchy[placetype].abbr); + + }); + + const esDoc = doc.toESDocument(); + esDoc.data._id = esDoc._id; + esDoc.data._type = esDoc._type; + + return esDoc.data; + + }); + +} + +function setup(placeholderService, should_execute) { + function controller( req, res, next ){ + if (!should_execute(req, res)) { + return next(); + } + + placeholderService.search(req.clean.text, req.clean.lang.iso6393, (err, results) => { + res.meta = {}; + res.data = _.flatten(results.map((result) => { + return synthesizeDocs(result); + })); + + return next(); + }); + + } + + return controller; +} + +module.exports = setup; diff --git a/controller/predicates/is_admin_only_analysis.js b/controller/predicates/is_admin_only_analysis.js new file mode 100644 index 00000000..4b3aef6a --- /dev/null +++ b/controller/predicates/is_admin_only_analysis.js @@ -0,0 +1,13 @@ +const _ = require('lodash'); + +module.exports = (request, response) => { + if (!request.clean.hasOwnProperty('parsed_text')) { + return false; + } + + // return true only if all non-admin properties of parsed_text are empty + return ['number', 'street', 'query', 'category'].every((prop) => { + return _.isEmpty(request.clean.parsed_text[prop]); + }); + +}; diff --git a/controller/predicates/is_pip_service_enabled.js b/controller/predicates/is_pip_service_enabled.js deleted file mode 100644 index aa787d87..00000000 --- a/controller/predicates/is_pip_service_enabled.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = (uri) => { - // this predicate relies upon the fact that the schema has already validated - // that api.pipService is a URI-formatted string - return (request, response) => { - return uri !== undefined; - }; -}; diff --git a/controller/predicates/is_service_enabled.js b/controller/predicates/is_service_enabled.js new file mode 100644 index 00000000..2e9a034f --- /dev/null +++ b/controller/predicates/is_service_enabled.js @@ -0,0 +1,5 @@ +module.exports = (uri) => { + return (request, response) => { + return uri !== undefined; + }; +}; diff --git a/package.json b/package.json index 79a598c0..475762fa 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "addressit": "1.5.0", "async": "^2.0.0", + "bl": "^1.2.0", "check-types": "^7.0.0", "elasticsearch": "^12.0.1", "elasticsearch-exceptions": "0.0.4", @@ -57,6 +58,7 @@ "morgan": "^1.8.2", "pelias-config": "2.10.0", "pelias-categories": "1.2.0", + "pelias-config": "2.10.0", "pelias-labels": "1.6.0", "pelias-logger": "0.2.0", "pelias-mock-logger": "^1.0.1", @@ -64,8 +66,8 @@ "pelias-query": "8.15.0", "pelias-text-analyzer": "1.8.2", "predicates": "^1.0.1", + "request": "^2.81.0", "retry": "^0.10.1", - "request": "^2.79.0", "stats-lite": "^2.0.4", "superagent": "^3.2.1", "through2": "^2.0.3" diff --git a/routes/v1.js b/routes/v1.js index 33f815e4..4aa92407 100644 --- a/routes/v1.js +++ b/routes/v1.js @@ -28,6 +28,7 @@ var controllers = { coarse_reverse: require('../controller/coarse_reverse'), mdToHTML: require('../controller/markdownToHtml'), place: require('../controller/place'), + placeholder: require('../controller/placeholder'), search: require('../controller/search'), status: require('../controller/status') }; @@ -66,6 +67,7 @@ var postProc = { 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'); // shorthand for standard early-exit conditions const hasResponseDataOrRequestErrors = any(hasResponseData, hasRequestErrors); @@ -79,13 +81,20 @@ const hasResponseDataOrRequestErrors = any(hasResponseData, hasRequestErrors); function addRoutes(app, peliasConfig) { const esclient = elasticsearch.Client(peliasConfig.esclient); - const isPipServiceEnabled = require('../controller/predicates/is_pip_service_enabled')(peliasConfig.api.pipService); + const isPipServiceEnabled = require('../controller/predicates/is_service_enabled')(peliasConfig.api.pipService); + const isPlaceholderServiceEnabled = require('../controller/predicates/is_service_enabled')(peliasConfig.api.placeholderService); + const pipService = require('../service/pointinpolygon')(peliasConfig.api.pipService); + const placeholderService = require('../service/placeholder')('http://localhost:3000/parser'); const coarse_reverse_should_execute = all( not(hasRequestErrors), isPipServiceEnabled, isCoarseReverse ); + const placeholderShouldExecute = all( + not(hasResponseDataOrRequestErrors), isPlaceholderServiceEnabled, isAdminOnlyAnalysis + ); + // execute under the following conditions: // - there are no errors or data // - request is not coarse OR pip service is disabled @@ -112,6 +121,7 @@ function addRoutes(app, peliasConfig) { sanitizers.search.middleware, 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)), @@ -143,7 +153,7 @@ function addRoutes(app, peliasConfig) { postProc.confidenceScores(peliasConfig.api), postProc.confidenceScoresFallback(), postProc.interpolate(), - postProc.dedupe(), + postProc.dedupe(), postProc.accuracy(), postProc.localNamingConventions(), postProc.renamePlacenames(), diff --git a/service/placeholder.js b/service/placeholder.js new file mode 100644 index 00000000..36447022 --- /dev/null +++ b/service/placeholder.js @@ -0,0 +1,62 @@ +const request = require('request'); +const bl = require('bl'); +const _ = require('lodash'); + +const logger = require( 'pelias-logger' ).get( 'placeholder' ); + +module.exports = function setup(url) { + if (_.isEmpty(url)) { + logger.warn('placeholder service disabled'); + + return { + search: (text, lang, callback) => { + callback(`placeholder service disabled`); + } + }; + + } + + logger.info(`using placeholder service at ${url}`); + return { + search: (text, lang, callback) => { + const requestUrl = `${url}/search?text=${text}&lang=${lang}`; + + request + .get(requestUrl) + .on('response', (response) => { + // pipe the response thru bl which will accumulate the entire body + response.pipe(bl((err, data) => { + if (response.statusCode === 200) { + // parse and return w/o error unless response wasn't JSON + try { + const parsed = JSON.parse(data); + return callback(null, parsed); + } + catch (err) { + logger.error(`${encodeURI(requestUrl)} could not parse response: ${data}`); + return callback(`${encodeURI(requestUrl)} could not parse response: ${data}`); + } + } + else if (response.statusCode === 404) { + // placeholder returns a 404 when no results are found which + // should be handled differently since it's technically not an error + logger.debug(`returned 0 results for '${text}'`); + return callback(null, []); + } + else { + // otherwise there was a non-200/404 status so handle generically + logger.error(`${encodeURI(requestUrl)} returned status ${response.statusCode}: ${data}`); + return callback(`${encodeURI(requestUrl)} returned status ${response.statusCode}: ${data}`); + } + })); + + }) + .on('error', (err) => { + logger.error(JSON.stringify(err)); + callback(err); + }); + + } + }; + +}; diff --git a/test/unit/controller/placeholder.js b/test/unit/controller/placeholder.js new file mode 100644 index 00000000..c972e5df --- /dev/null +++ b/test/unit/controller/placeholder.js @@ -0,0 +1,378 @@ +'use strict'; + +const placeholder = require('../../../controller/placeholder'); +const proxyquire = require('proxyquire').noCallThru(); + +module.exports.tests = {}; + +module.exports.tests.interface = (test, common) => { + test('valid interface', (t) => { + t.equal(typeof placeholder, 'function', 'placeholder is a function'); + t.equal(typeof placeholder(), 'function', 'placeholder returns a controller'); + t.end(); + }); +}; + +module.exports.tests.should_execute_failure = function(test, common) { + test('should_execute returning false should return without calling service', (t) => { + let placeholderService_was_called = false; + + const placeholderService = { + search: () => { + placeholderService_was_called = true; + } + }; + + const should_execute = (req, res) => { + // req and res should be passed to should_execute + t.deepEquals(req, { a: 1 }); + t.deepEquals(res, { b: 2 }); + return false; + }; + + const controller = placeholder(placeholderService, should_execute); + + const req = { a: 1 }; + const res = { b: 2 }; + + controller(req, res, () => { + t.notOk(placeholderService_was_called); + t.end(); + }); + + }); + +}; + +module.exports.tests.success = function(test, common) { + test('should_execute returning true should call service', (t) => { + let placeholderService_was_called = false; + + const placeholderService = { + search: (text, language, callback) => { + t.equals(text, 'query value'); + t.equals(language, 'language value'); + placeholderService_was_called = true; + callback(null, []); + } + }; + + const should_execute = (req, res) => { + return true; + }; + + const controller = placeholder(placeholderService, should_execute); + + const req = { + clean: { + text: 'query value', + lang: { + iso6393: 'language value' + } + } + }; + const res = { b: 2 }; + + controller(req, res, () => { + t.ok(placeholderService_was_called); + t.end(); + }); + + }); + + test('response from service should be converted', (t) => { + let placeholderService_was_called = false; + + const placeholder_response = [ + { + id: 123, + name: 'name 1', + placetype: 'neighbourhood', + lineage: [ + { + country: { + id: 1, + name: 'country name 1' + }, + dependency: { + id: 2, + name: 'dependency name 1' + }, + macroregion: { + id: 3, + name: 'macroregion name 1' + }, + region: { + id: 4, + name: 'region name 1' + }, + macrocounty: { + id: 5, + name: 'macrocounty name 1' + }, + county: { + id: 6, + name: 'county name 1' + }, + localadmin: { + id: 7, + name: 'localadmin name 1' + }, + locality: { + id: 8, + name: 'locality name 1' + }, + borough: { + id: 9, + name: 'borough name 1' + }, + neighbourhood: { + id: 10, + name: 'neighbourhood name 1' + } + }, + { + country: { + id: 11, + name: 'country name 2', + abbr: 'XYZ' + }, + dependency: { + id: 12, + name: 'dependency name 2', + abbr: 'dependency abbr 2' + }, + macroregion: { + id: 13, + name: 'macroregion name 2', + abbr: 'macroregion abbr 2' + }, + region: { + id: 14, + name: 'region name 2', + abbr: 'region abbr 2' + }, + macrocounty: { + id: 15, + name: 'macrocounty name 2', + abbr: 'macrocounty abbr 2' + }, + county: { + id: 16, + name: 'county name 2', + abbr: 'county abbr 2' + }, + localadmin: { + id: 17, + name: 'localadmin name 2', + abbr: 'localadmin abbr 2' + }, + locality: { + id: 18, + name: 'locality name 2', + abbr: 'locality abbr 2' + }, + borough: { + id: 19, + name: 'borough name 2', + abbr: 'borough abbr 2' + }, + neighbourhood: { + id: 20, + name: 'neighbourhood name 2', + abbr: 'neighbourhood abbr 2' + } + } + ], + geom: { + area: 12.34, + bbox: '21.212121,12.121212,31.313131,13.131313', + lat: 14.141414, + lon: 41.414141 + } + }, + { + id: 456, + name: 'name 3', + placetype: 'locality', + lineage: [ {} ], + geom: { + area: 23.45, + bbox: '51.515151,15.151515,61.616161,16.161616', + lat: 17.171717, + lon: 71.717171 + } + } + ]; + + const placeholderService = { + search: (text, language, callback) => { + t.equals(text, 'query value'); + t.equals(language, 'language value'); + placeholderService_was_called = true; + callback(null, placeholder_response); + } + }; + + const should_execute = (req, res) => { + return true; + }; + + const controller = placeholder(placeholderService, should_execute); + + const req = { + clean: { + text: 'query value', + lang: { + iso6393: 'language value' + } + } + }; + const res = { }; + + const expected_res = { + meta: {}, + data: [ + { + _id: '123', + _type: 'neighbourhood', + layer: 'neighbourhood', + source: 'whosonfirst', + source_id: '123', + center_point: { + lat: 14.141414, + lon: 41.414141 + }, + bounding_box: '{"min_lat":12.121212,"max_lat":13.131313,"min_lon":21.212121,"max_lon":31.313131}', + name: { + 'default': 'name 1' + }, + phrase: { + 'default': 'name 1' + }, + parent: { + neighbourhood: ['neighbourhood name 1'], + neighbourhood_id: ['10'], + neighbourhood_a: [null], + borough: ['borough name 1'], + borough_id: ['9'], + borough_a: [null], + locality: ['locality name 1'], + locality_id: ['8'], + locality_a: [null], + localadmin: ['localadmin name 1'], + localadmin_id: ['7'], + localadmin_a: [null], + county: ['county name 1'], + county_id: ['6'], + county_a: [null], + macrocounty: ['macrocounty name 1'], + macrocounty_id: ['5'], + macrocounty_a: [null], + region: ['region name 1'], + region_id: ['4'], + region_a: [null], + macroregion: ['macroregion name 1'], + macroregion_id: ['3'], + macroregion_a: [null], + dependency: ['dependency name 1'], + dependency_id: ['2'], + dependency_a: [null], + country: ['country name 1'], + country_id: ['1'], + country_a: [null] + } + }, + { + _id: '123', + _type: 'neighbourhood', + layer: 'neighbourhood', + source: 'whosonfirst', + source_id: '123', + center_point: { + lat: 14.141414, + lon: 41.414141 + }, + bounding_box: '{"min_lat":12.121212,"max_lat":13.131313,"min_lon":21.212121,"max_lon":31.313131}', + name: { + 'default': 'name 1' + }, + phrase: { + 'default': 'name 1' + }, + alpha3: 'XYZ', + parent: { + neighbourhood: ['neighbourhood name 2'], + neighbourhood_id: ['20'], + neighbourhood_a: ['neighbourhood abbr 2'], + borough: ['borough name 2'], + borough_id: ['19'], + borough_a: ['borough abbr 2'], + locality: ['locality name 2'], + locality_id: ['18'], + locality_a: ['locality abbr 2'], + localadmin: ['localadmin name 2'], + localadmin_id: ['17'], + localadmin_a: ['localadmin abbr 2'], + county: ['county name 2'], + county_id: ['16'], + county_a: ['county abbr 2'], + macrocounty: ['macrocounty name 2'], + macrocounty_id: ['15'], + macrocounty_a: ['macrocounty abbr 2'], + region: ['region name 2'], + region_id: ['14'], + region_a: ['region abbr 2'], + macroregion: ['macroregion name 2'], + macroregion_id: ['13'], + macroregion_a: ['macroregion abbr 2'], + dependency: ['dependency name 2'], + dependency_id: ['12'], + dependency_a: ['dependency abbr 2'], + country: ['country name 2'], + country_id: ['11'], + country_a: ['XYZ'] + } + }, + { + _id: '456', + _type: 'locality', + layer: 'locality', + source: 'whosonfirst', + source_id: '456', + center_point: { + lat: 17.171717, + lon: 71.717171 + }, + bounding_box: '{"min_lat":15.151515,"max_lat":16.161616,"min_lon":51.515151,"max_lon":61.616161}', + name: { + 'default': 'name 3' + }, + phrase: { + 'default': 'name 3' + }, + parent: { } + } + ] + }; + + controller(req, res, () => { + t.ok(placeholderService_was_called); + t.deepEquals(res, expected_res); + t.end(); + }); + + }); + +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('GET /placeholder ' + name, testFunction); + } + + for( const testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/unit/controller/predicates/is_admin_only_analysis.js b/test/unit/controller/predicates/is_admin_only_analysis.js new file mode 100644 index 00000000..eea8b567 --- /dev/null +++ b/test/unit/controller/predicates/is_admin_only_analysis.js @@ -0,0 +1,77 @@ +'use strict'; + +const _ = require('lodash'); +const is_admin_only_analysis = require('../../../../controller/predicates/is_admin_only_analysis'); + +module.exports.tests = {}; + +module.exports.tests.interface = (test, common) => { + test('valid interface', (t) => { + t.equal(typeof is_admin_only_analysis, 'function', 'is_admin_only_analysis is a function'); + t.end(); + }); +}; + +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) => { + const req = { + clean: { + parsed_text: {} + } + }; + const res = {}; + + req.clean.parsed_text[property] = `${property} value`; + + t.ok(is_admin_only_analysis(req, res)); + + }); + t.end(); + + }); + +}; + +module.exports.tests.false_conditions = (test, common) => { + test('req.clean with no parsed_text should return false', (t) => { + const req = { + clean: { + } + }; + const res = {}; + + t.notOk(is_admin_only_analysis(req, res)); + t.end(); + + }); + + test('parsed_text with non-admin properties should return false', (t) => { + ['number', 'street', 'query', 'category'].forEach((property) => { + const req = { + clean: { + parsed_text: {} + } + }; + const res = {}; + + req.clean.parsed_text[property] = `${property} value`; + + t.notOk(is_admin_only_analysis(req, res)); + + }); + t.end(); + + }); + +}; + +module.exports.all = (tape, common) => { + function test(name, testFunction) { + return tape(`GET /is_admin_only_analysis ${name}`, testFunction); + } + + for( const testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/unit/controller/predicates/is_pip_service_enabled.js b/test/unit/controller/predicates/is_service_enabled.js similarity index 58% rename from test/unit/controller/predicates/is_pip_service_enabled.js rename to test/unit/controller/predicates/is_service_enabled.js index 1d3cd63a..f7f99748 100644 --- a/test/unit/controller/predicates/is_pip_service_enabled.js +++ b/test/unit/controller/predicates/is_service_enabled.js @@ -1,21 +1,21 @@ 'use strict'; const _ = require('lodash'); -const is_pip_service_enabled = require('../../../../controller/predicates/is_pip_service_enabled'); +const is_service_enabled = require('../../../../controller/predicates/is_service_enabled'); module.exports.tests = {}; module.exports.tests.interface = (test, common) => { test('valid interface', (t) => { - t.equal(typeof is_pip_service_enabled, 'function', 'is_pip_service_enabled is a function'); - t.equal(typeof is_pip_service_enabled(), 'function', 'is_pip_service_enabled() is a function'); + t.equal(typeof is_service_enabled, 'function', 'is_service_enabled is a function'); + t.equal(typeof is_service_enabled(), 'function', 'is_service_enabled() is a function'); t.end(); }); }; module.exports.tests.true_conditions = (test, common) => { test('string uri should return true', (t) => { - t.ok(is_pip_service_enabled('pip uri')()); + t.ok(is_service_enabled('pip uri')()); t.end(); }); @@ -24,7 +24,7 @@ module.exports.tests.true_conditions = (test, common) => { module.exports.tests.false_conditions = (test, common) => { test('undefined uri should return false', (t) => { - t.notOk(is_pip_service_enabled()()); + t.notOk(is_service_enabled()()); t.end(); }); @@ -33,7 +33,7 @@ module.exports.tests.false_conditions = (test, common) => { module.exports.all = (tape, common) => { function test(name, testFunction) { - return tape(`GET /is_pip_service_enabled ${name}`, testFunction); + return tape(`GET /is_service_enabled ${name}`, testFunction); } for( const testCase in module.exports.tests ){ diff --git a/test/unit/run.js b/test/unit/run.js index 3c7d01e4..f5638dba 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -14,11 +14,13 @@ var tests = [ require('./controller/coarse_reverse'), require('./controller/index'), require('./controller/place'), + require('./controller/placeholder'), require('./controller/search'), require('./controller/predicates/has_response_data'), require('./controller/predicates/has_request_errors'), + require('./controller/predicates/is_admin_only_analysis'), require('./controller/predicates/is_coarse_reverse'), - require('./controller/predicates/is_pip_service_enabled'), + require('./controller/predicates/is_service_enabled'), require('./helper/diffPlaces'), require('./helper/geojsonify'), require('./helper/logging'), @@ -81,6 +83,7 @@ var tests = [ require('./service/mget'), require('./service/search'), require('./service/interpolation'), + require('./service/placeholder'), require('./service/pointinpolygon'), require('./service/language') ]; diff --git a/test/unit/service/placeholder.js b/test/unit/service/placeholder.js new file mode 100644 index 00000000..23b00bbc --- /dev/null +++ b/test/unit/service/placeholder.js @@ -0,0 +1,193 @@ +const proxyquire = require('proxyquire').noCallThru(); +const express = require('express'); + +const setup = require('../../../service/placeholder'); + +module.exports.tests = {}; + +module.exports.tests.interface = (test, common) => { + test('valid interface', (t) => { + const logger = require('pelias-mock-logger')(); + + var service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + }); + + t.equal(typeof service, 'function', 'service is a function'); + t.end(); + }); +}; + +module.exports.tests.do_nothing_service = (test, common) => { + test('undefined url should return service that logs fact that placeholder service is not available', (t) => { + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(); + + service.search('search text', 'search lang', (err) => { + t.deepEquals(logger.getWarnMessages(), [ + 'placeholder service disabled' + ]); + t.equals(err, 'placeholder service disabled'); + t.end(); + }); + + }); + +}; + +module.exports.tests.failure_conditions = (test, common) => { + test('server returning error should log it and return no results', (t) => { + const server = express().listen(); + const port = server.address().port; + + // immediately close the server so to ensure an error response + server.close(); + + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(`http://localhost:${port}`); + + service.search('search text', 'search lang', (err, results) => { + t.equals(err.code, 'ECONNREFUSED'); + t.notOk(results); + t.ok(logger.isErrorMessage(/ECONNREFUSED/), 'there should be a connection refused error message'); + t.end(); + + server.close(); + + }); + + }); + + test('server returning non-200/404 response should log error and return no results', (t) => { + const placeholderServer = express(); + placeholderServer.get('/search', (req, res, next) => { + res.status(400).send('a bad request was made'); + }); + + const server = placeholderServer.listen(); + const port = server.address().port; + + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(`http://localhost:${port}`); + + service.search('search text', 'search lang', (err, results) => { + t.equals(err, `http://localhost:${port}/search?text=search%20text&lang=search%20lang returned status 400: a bad request was made`); + t.notOk(results); + t.ok(logger.isErrorMessage(`http://localhost:${port}/search?text=search%20text&lang=search%20lang ` + + `returned status 400: a bad request was made`)); + t.end(); + + server.close(); + + }); + + }); + + test('server returning 404 statusCode should log debug message and return no error or results', (t) => { + const placeholderServer = express(); + placeholderServer.get('/search', (req, res, next) => { + res.status(404).send('no results found'); + }); + + const server = placeholderServer.listen(); + const port = server.address().port; + + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(`http://localhost:${port}`); + + service.search('search text', 'search lang', (err, results) => { + t.notOk(err); + t.deepEquals(results, [], 'should return an empty array'); + t.ok(logger.isDebugMessage('returned 0 results for \'search text\'')); + t.end(); + + server.close(); + + }); + + }); + + test('server returning 200 statusCode but with non-JSON response should log error and return undefined', (t) => { + const placeholderServer = express(); + placeholderServer.get('/search', (req, res, next) => { + res.status(200).send('this is not parseable as JSON'); + }); + + const server = placeholderServer.listen(); + const port = server.address().port; + + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(`http://localhost:${port}`); + + service.search('search text', 'search lang', (err, results) => { + t.equals(err, `http://localhost:${port}/search?text=search%20text&lang=search%20lang ` + + `could not parse response: this is not parseable as JSON`); + t.notOk(results, 'should return undefined'); + t.ok(logger.isErrorMessage(`http://localhost:${port}/search?text=search%20text&lang=search%20lang ` + + `could not parse response: this is not parseable as JSON`)); + t.end(); + + server.close(); + + }); + + }); + +}; + +module.exports.tests.success_conditions = (test, common) => { + test('server returning statusCode 200 should return no error and parsed output', (t) => { + const placeholderServer = express(); + placeholderServer.get('/search', (req, res, next) => { + if (req.query.text === 'search text' && req.query.lang === 'search lang') { + res.status(200).send('[1, 2, 3]'); + } + }); + + const server = placeholderServer.listen(); + const port = server.address().port; + + const logger = require('pelias-mock-logger')(); + + const service = proxyquire('../../../service/placeholder', { + 'pelias-logger': logger + })(`http://localhost:${port}`); + + service.search('search text', 'search lang', (err, results) => { + t.notOk(err, 'should be no error'); + t.deepEquals(results, [1, 2, 3]); + t.notOk(logger.hasErrorMessages()); + t.end(); + + server.close(); + + }); + + }); + +}; + +module.exports.all = (tape, common) => { + function test(name, testFunction) { + return tape(`SERVICE /placeholder ${name}`, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +};