diff --git a/configValidation.js b/configValidation.js new file mode 100644 index 000000000..eca2f84a8 --- /dev/null +++ b/configValidation.js @@ -0,0 +1,26 @@ +'use strict'; + +const Joi = require('joi'); + +// Schema Configuration +// datapath: string (required) +// files: array of strings +// deduplicate: boolean +// adminLookup: boolean +const schema = Joi.object().keys({ + files: Joi.array().items(Joi.string()), + datapath: Joi.string(), + deduplicate: Joi.boolean(), + adminLookup: Joi.boolean() +}).requiredKeys('datapath').unknown(false); + +module.exports = { + validate: function validate(config) { + Joi.validate(config, schema, (err) => { + if (err) { + throw new Error(err.details[0].message); + } + }); + } + +}; diff --git a/import.js b/import.js index 95aebd637..329ad03b8 100644 --- a/import.js +++ b/import.js @@ -5,6 +5,9 @@ 'use strict'; var peliasConfig = require( 'pelias-config' ).generate(); + +require('./configValidation').validate(peliasConfig); + var logger = require( 'pelias-logger' ).get( 'openaddresses' ); var parameters = require( './lib/parameters' ); diff --git a/package.json b/package.json index 93c107dd3..508394eb4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "combined-stream": "1.0.5", "csv-parse": "^1.1.7", "glob": "^7.0.0", + "joi": "^10.1.0", "lodash": "^4.10.0", "minimist": "1.2.0", "pelias-address-deduplicator": "1.1.0", @@ -28,6 +29,7 @@ "event-stream": "^3.3.2", "jshint": "^2.9.4", "precommit-hook": "3.0.0", + "proxyquire": "^1.7.10", "tap-spec": "4.1.1", "tape": "^4.5.0", "temp": "^0.8.3", diff --git a/test/configValidation.js b/test/configValidation.js new file mode 100644 index 000000000..7662cdcbe --- /dev/null +++ b/test/configValidation.js @@ -0,0 +1,130 @@ +'use strict'; + +const tape = require( 'tape' ); + +const configValidation = require( '../configValidation' ); + +tape( 'missing datapath should throw error', function(test) { + const config = {}; + + test.throws(() => { + configValidation.validate(config); + }, /"datapath" is required/); + + test.end(); +}); + +tape( 'non-string datapath should throw error', function(test) { + [null, 17, {}, [], false].forEach((value) => { + const config = { + datapath: value + }; + + test.throws(() => { + configValidation.validate(config); + }, /"datapath" must be a string/); + + }); + + test.end(); +}); + +tape( 'non-array files should throw error', function(test) { + [null, 17, {}, 'string', false].forEach((value) => { + const config = { + datapath: 'this is the datapath', + files: value + }; + + test.throws(() => { + configValidation.validate(config); + }, /"files" must be an array/, 'datapath is required'); + }); + + test.end(); +}); + +tape( 'non-string elements in files array should throw error', function(test) { + [null, 17, {}, [], false].forEach((value) => { + const config = { + datapath: 'this is the datapath', + files: [value] + }; + + test.throws(() => { + configValidation.validate(config); + }, /"0" must be a string/, 'files elements must be strings'); + }); + + test.end(); +}); + +tape( 'non-boolean adminLookup should throw error', function(test) { + [null, 17, {}, [], 'string'].forEach((value) => { + const config = { + datapath: 'this is the datapath', + adminLookup: value + }; + + test.throws(() => { + configValidation.validate(config); + }, /"adminLookup" must be a boolean/); + }); + + test.end(); +}); + +tape( 'non-boolean deduplicate should throw error', function(test) { + [null, 17, {}, [], 'string'].forEach((value) => { + const config = { + datapath: 'this is the datapath', + deduplicate: value + }; + + test.throws(() => { + configValidation.validate(config); + }, /"deduplicate" must be a boolean/); + }); + + test.end(); +}); + +tape( 'unknown config fields should throw error', function(test) { + const config = { + datapath: 'this is the datapath', + unknown: 'value' + }; + + test.throws(() => { + configValidation.validate(config); + }, /"unknown" is not allowed/, 'unknown fields should be disallowed'); + test.end(); + +}); + +tape( 'configuration with only datapath should not throw error', function(test) { + const config = { + datapath: 'this is the datapath' + }; + + test.doesNotThrow(() => { + configValidation.validate(config); + }, 'config should be valid'); + test.end(); + +}); + +tape( 'valid configuration should not throw error', function(test) { + const config = { + datapath: 'this is the datapath', + deduplicate: false, + adminLookup: false, + files: ['file 1', 'file 2'] + }; + + test.doesNotThrow(() => { + configValidation.validate(config); + }, 'config should be valid'); + test.end(); + +}); diff --git a/test/import.js b/test/import.js new file mode 100644 index 000000000..9e2591165 --- /dev/null +++ b/test/import.js @@ -0,0 +1,21 @@ +'use strict'; + +const tape = require( 'tape' ); + +const proxyquire = require('proxyquire').noCallThru(); + +tape( 'configValidation throwing error should rethrow', function(test) { + test.throws(function() { + proxyquire('../import', { + './configValidation': { + validate: () => { + throw Error('config is not valid'); + } + } + })(); + + }, /config is not valid/); + + test.end(); + +}); diff --git a/test/test.js b/test/test.js index f180ada42..72111d640 100644 --- a/test/test.js +++ b/test/test.js @@ -4,8 +4,10 @@ 'use strict'; +require( './configValidation' ); require( './isValidCsvRecord' ); require( './streams/adminLookupStream'); +require( './import'); require( './importPipeline'); require( './parameters' ); require( './streams/cleanupStream' );