diff --git a/.gitignore b/.gitignore index ab05030f..419c501c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules -*.log \ No newline at end of file +coverage +.idea +*.log +reports \ No newline at end of file diff --git a/.jshintignore b/.jshintignore index 3c3629e6..ab14aab9 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1 +1,3 @@ node_modules +coverage +reports diff --git a/README.md b/README.md index 01da637a..9c5c1121 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ $ npm test $ npm run docs ``` +### Code Coverage + +```bash +$ npm run coverage +``` + ### Continuous Integration Travis tests every release against node version `0.10` diff --git a/package.json b/package.json index 0276211e..4f35bbe2 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "main": "index.js", "scripts": { "start": "node index.js", - "test": "npm run unit", + "test": "npm run unit && npm run coverage", "unit": "node test/unit/run.js | tap-spec", "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao", + "coverage": "node_modules/.bin/istanbul cover test/unit/run.js", "audit": "npm shrinkwrap; node node_modules/nsp/bin/nspCLI.js audit-shrinkwrap; rm npm-shrinkwrap.json;", "docs": "rm -r docs; cd test/ciao; node ../../node_modules/ciao/bin/ciao -c ../ciao.json . -d ../../docs" }, @@ -44,6 +45,7 @@ }, "devDependencies": { "ciao": "^0.3.4", + "istanbul": "^0.3.13", "jshint": "^2.5.6", "nsp": "^0.3.0", "precommit-hook": "^1.0.7", diff --git a/query/reverse.js b/query/reverse.js index 35b86ade..499ddddc 100644 --- a/query/reverse.js +++ b/query/reverse.js @@ -1,7 +1,6 @@ -var logger = require('../src/logger'), - queries = require('geopipes-elasticsearch-backend').queries, - sort = require('../query/sort'); +var queries = require('geopipes-elasticsearch-backend').queries, + sort = require('./sort'); function generate( params ){ @@ -13,7 +12,17 @@ function generate( params ){ var query = queries.distance( centroid, { size: params.size || 1 } ); query.sort = query.sort.concat(sort); + if ( params.categories && params.categories.length > 0 ) { + addCategoriesFilter( query, params.categories ); + } + return query; } +function addCategoriesFilter( query, categories ) { + query.query.filtered.filter.bool.must.push({ + terms: { category: categories } + }); +} + module.exports = generate; \ No newline at end of file diff --git a/sanitiser/_categories.js b/sanitiser/_categories.js new file mode 100644 index 00000000..f29c9fd0 --- /dev/null +++ b/sanitiser/_categories.js @@ -0,0 +1,38 @@ + +var isObject = require('is-object'); + +// validate inputs, convert types and apply defaults +function sanitize( req ){ + + var clean = req.clean || {}; + var params= req.query; + + // ensure the input params are a valid object + if( !isObject( params ) ){ + params = {}; + } + + // default case (no categories specified in GET params) + if('string' !== typeof params.categories || !params.categories.length){ + clean.categories = []; + } + else { + // parse GET params + clean.categories = params.categories.split(',') + .map(function (cat) { + return cat.toLowerCase().trim(); // lowercase inputs + }) + .filter( function( cat ) { + return ( cat.length > 0 ); + }); + } + + // pass validated params to next middleware + req.clean = clean; + + return { 'error': false }; + +} + +// export function +module.exports = sanitize; diff --git a/sanitiser/reverse.js b/sanitiser/reverse.js index 117b67d2..9804b4e9 100644 --- a/sanitiser/reverse.js +++ b/sanitiser/reverse.js @@ -1,6 +1,5 @@ -var logger = require('../src/logger'), - _sanitize = require('../sanitiser/_sanitize'), +var _sanitize = require('../sanitiser/_sanitize'), sanitiser = { latlonzoom: function( req ) { var geo = require('../sanitiser/_geo'); @@ -10,6 +9,10 @@ var logger = require('../src/logger'), size: function( req ) { var size = require('../sanitiser/_size'); return size(req, 1); + }, + categories: function ( req ) { + var categories = require('../sanitiser/_categories'); + return categories(req); } }; diff --git a/test/unit/query/reverse.js b/test/unit/query/reverse.js index 0380fb85..00a3d15e 100644 --- a/test/unit/query/reverse.js +++ b/test/unit/query/reverse.js @@ -111,6 +111,71 @@ module.exports.tests.query = function(test, common) { }); t.end(); }); + + test('valid query with categories', function(t) { + var params = { lat: 29.49136, lon: -82.50622, categories: ['food', 'education', 'entertainment'] }; + var query = generate(params); + + var expected = { + 'query': { + 'filtered': { + 'query': { + 'match_all': {} + }, + 'filter': { + 'bool': { + 'must': [ + { + 'geo_distance': { + 'distance': '50km', + 'distance_type': 'plane', + 'optimize_bbox': 'indexed', + '_cache': true, + 'center_point': { + 'lat': '29.49', + 'lon': '-82.51' + } + } + }, + { + 'terms': { + 'category': params.categories + } + } + ] + } + } + } + }, + 'sort': [ + '_score', + { + '_geo_distance': { + 'center_point': { + 'lat': 29.49136, + 'lon': -82.50622 + }, + 'order': 'asc', + 'unit': 'km' + } + } + ].concat(sort.slice(1)), + 'size': 1, + 'track_scores': true + }; + + t.deepEqual(query, expected, 'valid reverse query with categories'); + + // test different sizes + var sizes = [1,2,10,undefined,null]; + sizes.forEach( function(size) { + params.size = size; + query = generate(params); + expected.size = size ? size : 1; + t.deepEqual(query, expected, 'valid reverse query for size: '+ size); + }); + t.end(); + }); }; module.exports.all = function (tape, common) { diff --git a/test/unit/sanitiser/reverse.js b/test/unit/sanitiser/reverse.js index 20ecbe0c..38417103 100644 --- a/test/unit/sanitiser/reverse.js +++ b/test/unit/sanitiser/reverse.js @@ -8,7 +8,8 @@ var suggest = require('../../../sanitiser/reverse'), layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin', 'osmaddress', 'openaddresses' ], lon: 0, - size: 1 + size: 1, + categories: [] }, sanitize = function(query, cb) { _sanitize({'query':query}, cb); }; @@ -200,6 +201,45 @@ module.exports.tests.sanitize_layers = function(test, common) { }); }; +module.exports.tests.sanitize_categories = function(test, common) { + var queryParams = { input: 'test', lat: 0, lon: 0 }; + test('unspecified', function(t) { + queryParams.categories = undefined; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, defaultClean.categories, 'default to empty categories array'); + t.end(); + }); + }); + test('single category', function(t) { + queryParams.categories = 'food'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food'], 'category set'); + t.end(); + }); + }); + test('multiple categories', function(t) { + queryParams.categories = 'food,education,nightlife'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food', 'education', 'nightlife'], 'categories set'); + t.end(); + }); + }); + test('whitespace and empty strings', function(t) { + queryParams.categories = 'food, , nightlife ,'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food', 'nightlife'], 'categories set'); + t.end(); + }); + }); + test('all empty strings', function(t) { + queryParams.categories = ', , ,'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, defaultClean.categories, 'empty strings filtered out'); + t.end(); + }); + }); +}; + module.exports.tests.middleware_failure = function(test, common) { test('middleware failure', function(t) { var res = { status: function( code ){