UNPKG

@luminati-io/mountebank

Version:

Over the wire test doubles

187 lines (160 loc) 6.43 kB
'use strict'; const exceptions = require('../util/errors.js'), helpers = require('../util/helpers.js'); /** * The module that does validation of behavior configuration * @module */ /** * Creates the validator * @returns {{validate: validate}} */ function create () { function hasExactlyOneKey (obj) { const keys = Object.keys(obj); return keys.length === 1; } function navigate (config, path) { if (path === '') { return config; } else { return path.split('.').reduce(function (field, fieldName) { return field[fieldName]; }, config); } } function typeErrorMessageFor (allowedTypes, additionalContext) { const spellings = { number: 'a', object: 'an', string: 'a' }; let message = `must be ${spellings[allowedTypes[0]]} ${allowedTypes[0]}`; for (let i = 1; i < allowedTypes.length; i += 1) { message += ` or ${spellings[allowedTypes[i]]} ${allowedTypes[i]}`; } if (additionalContext) { message += `, representing ${additionalContext}`; } return message; } function pathFor (pathPrefix, fieldName) { if (pathPrefix === '') { return fieldName; } else { return `${pathPrefix}.${fieldName}`; } } function nonMetadata (fieldName) { return fieldName.indexOf('_') !== 0; } function isTopLevelSpec (spec) { // True of copy and lookup behaviors that define the metadata below the top level keys return helpers.isObject(spec) && Object.keys(spec).filter(nonMetadata).length === Object.keys(spec).length; } function enumFieldFor (field) { const isObject = helpers.isObject; // Can be the string value or the object key if (isObject(field) && Object.keys(field).length > 0) { return Object.keys(field)[0]; } else { return field; } } function matchesEnum (field, enumSpec) { return enumSpec.indexOf(enumFieldFor(field)) >= 0; } function addMissingFieldError (fieldSpec, path, addErrorFn) { // eslint-disable-next-line no-underscore-dangle if (fieldSpec._required) { addErrorFn(path, 'required'); } } function addTypeErrors (fieldSpec, path, field, config, addErrorFn) { /* eslint complexity: 0 */ const fieldType = typeof field, allowedTypes = Object.keys(fieldSpec._allowedTypes), // eslint-disable-line no-underscore-dangle typeSpec = fieldSpec._allowedTypes[fieldType]; // eslint-disable-line no-underscore-dangle if (!helpers.defined(typeSpec)) { addErrorFn(path, typeErrorMessageFor(allowedTypes, fieldSpec._additionalContext)); // eslint-disable-line no-underscore-dangle } else { if (typeSpec.singleKeyOnly && !hasExactlyOneKey(field)) { addErrorFn(path, 'must have exactly one key'); } else if (typeSpec.enum && !matchesEnum(field, typeSpec.enum)) { addErrorFn(path, `must be one of [${typeSpec.enum.join(', ')}]`); } else if (typeSpec.nonNegativeInteger && field < 0) { addErrorFn(path, 'must be an integer greater than or equal to 0'); } else if (typeSpec.positiveInteger && field <= 0) { addErrorFn(path, 'must be an integer greater than 0'); } addErrorsFor(config, path, fieldSpec, addErrorFn); } } function addErrorsFor (config, pathPrefix, spec, addErrorFn) { Object.keys(spec).filter(nonMetadata).forEach(fieldName => { const fieldSpec = spec[fieldName], path = pathFor(pathPrefix, fieldName), field = navigate(config, path); if (!helpers.defined(field)) { addMissingFieldError(fieldSpec, path, addErrorFn); } else if (isTopLevelSpec(fieldSpec)) { // Recurse but reset pathPrefix so error message is cleaner // e.g. 'copy behavior "from" field required' instead of 'copy behavior "copy.from" field required' addErrorsFor(field, '', fieldSpec, addErrorFn); } else { addTypeErrors(fieldSpec, path, field, config, addErrorFn); } }); } /** * Validates the behavior configuration and returns all errors * @memberOf module:models/behaviorsValidator# * @param {Object} behaviors - The behaviors list * @param {Object} validationSpec - the specification to validate against * @returns {Object} The array of errors */ function validate (behaviors, validationSpec) { const errors = []; (behaviors || []).forEach(config => { const validBehaviors = [], unrecognizedKeys = []; Object.keys(config).forEach(key => { const addError = function (field, message, subConfig) { errors.push(exceptions.ValidationError(`${key} behavior "${field}" field ${message}`, { source: subConfig || config })); }, spec = {}; if (validationSpec[key]) { validBehaviors.push(key); spec[key] = validationSpec[key]; addErrorsFor(config, '', spec, addError); } else { unrecognizedKeys.push({ key: key, source: config }); } }); // Allow adding additional custom fields to valid behaviors but ensure there is a valid behavior if (validBehaviors.length === 0 && unrecognizedKeys.length > 0) { errors.push(exceptions.ValidationError(`Unrecognized behavior: "${unrecognizedKeys[0].key}"`, { source: unrecognizedKeys[0].source })); } if (validBehaviors.length > 1) { errors.push(exceptions.ValidationError('Each behavior object must have only one behavior type', { source: config })); } }); return errors; } return { validate }; } module.exports = { create };