Commit 0197ceff authored by Stefan Cameron's avatar Stefan Cameron

Hardening, clean-up, but fixes, improvements

While updating the README,

- found a few minor issues which I fixed

- updated the API docs

- added more integration tests (to verify README examples)

- added support for regexp testing to STRING_args, fixing issue
  #2

- custom validators can now return truthy values (or nothing) to
  pass verification, or either throw errors or return falsy values
  to fail verification

- renamed 'FlagSpec' to 'Flags' in various argument properties
  since that seemed more intuitive, and it's closer to
  `RegExp`'s `flags` argument, which is what it relates to
parent 26eee8da
This diff is collapsed.
......@@ -381,12 +381,40 @@ const extractNextType = function(typeset, qualifier) {
return subtype;
};
/**
* [Internal] Invokes a custom validator function found in a typeset.
* @private
* @function rtvref.impl._callCustomValidator
* @param {rtvref.types.custom_validator} validator Custom validator to invoke.
* @param {*} value Value being verified.
* @param {rtvref.types.fully_qualified_typeset} match Fully-qualified typeset
* for the subtype of `typeset` that matched.
* @param {rtvref.types.typeset} typeset Typeset used for verification.
* @returns {(undefined|Error)} `undefined` if the validator succeeded; `Error`
* if the validator failed.
*/
const _callCustomValidator = function(validator, value, match, typeset) {
let failure;
try {
const result = validator(value, match, typeset);
if (result !== undefined && !result) { // undefined === no action === success
failure = new Error('Verification failed because of the custom validator');
}
} catch (err) {
failure = err;
}
return failure;
};
/**
* [Internal] Common options for the various `check*()` functions.
* @private
* @typedef {Object} rtvref.impl._checkOptions
* @property {Array.<string>} path The current path into the typeset. Initially
* empty to signify the root (top-level) value being checked.
* @property {Array.<string>} path The current path into the original typeset.
* Initially empty to signify the root (top-level) value being checked.
* @property {boolean} isTypeset `true` if the typeset specified in the public
* parameters has already been validated and is a valid __shallow__ typeset;
* `false` otherwise (which means the typeset should first be validated before
......@@ -623,9 +651,8 @@ const checkWithArray = function(value, array /*, options*/) {
// check for a validator at the end of the Array typeset and invoke it
const lastType = array[array.length - 1];
if (isCustomValidator(lastType)) {
try {
lastType(value, match, array); // invoke it
} catch (failure) {
const failure = _callCustomValidator(lastType, value, match, array);
if (failure !== undefined) {
// invalid in spite of the match since the validator said no
err = new RtvError(value, array, options.path, fullyQualify(array, qualifier), failure);
}
......@@ -686,11 +713,10 @@ const check = function(value, typeset /*, options*/) {
// the subtype within the implied typeset that matched
const match = fullyQualify(impliedType, options.qualifier);
// call the custom validator
try {
typeset(value, match, typeset);
} catch (failure) {
return new RtvError(value, typeset, options.path, match, failure);
const failure = _callCustomValidator(typeset, value, match, typeset);
if (failure !== undefined) {
return new RtvError(value, typeset, options.path,
fullyQualify(typeset, options.qualifier), failure);
}
return new RtvSuccess();
......@@ -751,6 +777,7 @@ const impl = {
// internal
_validatorMap, // exposed mainly to support unit testing
_registerType,
_callCustomValidator,
_getCheckOptions,
// public
getQualifier,
......
......@@ -54,7 +54,7 @@ const REQUIRED = '!';
* @see {@link rtvref.types}
* @see {@link rtvref.types.STRING}
*/
const EXPECTED = '+';
const EXPECTED = '*';
/**
* Optional qualifier: The value _may_ be of the expected type. Depending on
......
This diff is collapsed.
......@@ -76,13 +76,13 @@ export default function valHashMap(v, q = REQUIRED, args) {
// get the key expression
const keyExp = (args.keyExp && isString(args.keyExp)) ? args.keyExp : undefined;
// get the key expression flags only if we have a key expression
const keyFlagSpec = (keyExp && args.keyFlagSpec && isString(args.keyFlagSpec)) ?
args.keyFlagSpec : undefined;
const keyFlags = (keyExp && args.keyFlags && isString(args.keyFlags)) ?
args.keyFlags : undefined;
// get the typeset for values
const tsValues = isTypeset(args.values) ? args.values : undefined;
if (keyExp || tsValues) {
const reKeys = keyExp ? new RegExp(keyExp, keyFlagSpec) : undefined;
const reKeys = keyExp ? new RegExp(keyExp, keyFlags) : undefined;
_forEach(keys, function(key) {
const value = v[key];
......
......@@ -89,13 +89,13 @@ export default function valMap(v, q = REQUIRED, args) {
const keyExp = (tsKeysIsString && args.keyExp && isString(args.keyExp)) ?
args.keyExp : undefined;
// get the key expression flags only if we have a key expression
const keyFlagSpec = (keyExp && args.keyFlagSpec && isString(args.keyFlagSpec)) ?
args.keyFlagSpec : undefined;
const keyFlags = (keyExp && args.keyFlags && isString(args.keyFlags)) ?
args.keyFlags : undefined;
// get the typeset for values
const tsValues = isTypeset(args.values) ? args.values : undefined;
if (tsKeys || tsValues) {
const reKeys = keyExp ? new RegExp(keyExp, keyFlagSpec) : undefined;
const reKeys = keyExp ? new RegExp(keyExp, keyFlags) : undefined;
const it = v.entries(); // iterator
for (let elem of it) {
......
......@@ -61,36 +61,61 @@ export default function valString(v, q = REQUIRED, args) {
return new RtvSuccess();
}
let valid = isString(v) || (q !== REQUIRED && v === '');
// start by ensuring the value is a string, but allow an empty string for now
let valid = isString(v, {allowEmpty: true});
// if an arg allows the string to be empty, overriding the qualifier
let argsAllowEmpty = false;
if (valid && args) { // then check args
// empty string is OK for 'oneOf'
if (isString(args.oneOf) || (isArray(args.oneOf) && args.oneOf.length > 0)) {
const possibilities = [].concat(args.oneOf);
// flip the result so that valid is set to false if no values match
valid = !possibilities.every(function(possibility) {
// return false on first match to break the loop
return !(isString(possibility) && v === possibility);
});
if (args.exp && isString(args.exp)) { // all other args except expFlags are ignored
const flagSpec = (args.expFlags && isString(args.expFlags)) ?
args.expFlags : undefined;
const re = new RegExp(args.exp, flagSpec);
valid = re.test(v);
argsAllowEmpty = (valid && v === '');
} else {
let min;
if (valid && isFinite(args.min) && args.min >= 0) {
min = args.min;
valid = (v.length >= min);
}
// empty string is OK for 'oneOf'
if (isString(args.oneOf, {allowEmpty: true}) ||
(isArray(args.oneOf) && args.oneOf.length > 0)) {
if (valid && isFinite(args.max) && args.max >= 0) {
if (min === undefined || args.max >= min) {
valid = (v.length <= args.max);
} // else, ignore
}
const possibilities = [].concat(args.oneOf);
// flip the result so that valid is set to false if no values match
valid = !possibilities.every(function(possibility) {
// return false on first match to break the loop
return !(isString(possibility, {allowEmpty: true}) && v === possibility);
});
argsAllowEmpty = (valid && v === '');
} else {
let min;
if (valid && isFinite(args.min) && args.min >= 0) {
min = args.min;
valid = (v.length >= min);
argsAllowEmpty = (valid && v === '');
}
if (valid && args.partial) {
valid = v.includes(args.partial);
if (valid && isFinite(args.max) && args.max >= 0) {
if (min === undefined || args.max >= min) {
valid = (v.length <= args.max);
argsAllowEmpty = (valid && v === '');
} // else, ignore
}
if (valid && isString(args.partial)) {
valid = v.includes(args.partial);
// NOTE: partial doesn't apply to argsAllowEmpty because it's ignored
// if it's empty
}
}
}
}
// only REQUIRED qualifier disallows empty strings by default unless an
// arg overrides it
if (valid && !argsAllowEmpty && q === REQUIRED) {
valid = (v !== '');
}
if (valid) {
return new RtvSuccess();
}
......
......@@ -75,6 +75,15 @@ const rtv = {
return isTypeset;
},
/**
* Fully-qualifies a given typeset.
* @function rtv.fullyQualify
* @see {@link rtvref.impl.fullyQualify}
*/
get fullyQualify() {
return impl.fullyQualify;
},
/**
* Shortcut proxy for reading {@link rtv.config.enabled}.
* @readonly
......@@ -160,6 +169,9 @@ const rtv = {
throw result; // expected to be an RtvError
}
// NOTE: this method still returns a truthy value so that expressions like
// `rtv.e && rtv.verify(...) && do_something_on_success()` work when this
// method doesn't throw an exception
return new RtvSuccess();
},
......@@ -169,7 +181,8 @@ const rtv = {
*/
config: Object.defineProperties({}, {
/**
* Globally enables or disables {@link rtv.verify} and {@link rtv.check}.
* Globally enables or disables {@link rtv.verify} and {@link rtv.check}. When set
* to `false`, these methods are no-ops.
*
* Use this, or the shortcut {@link rtv.e}, to enable code optimization
* when building source with a bundler that supports _tree shaking_, like
......
......@@ -4,17 +4,54 @@ import {expect} from 'chai';
import rtv from '../src/rtv';
import impl from '../src/lib/impl';
import RtvSuccess from '../src/lib/RtvSuccess';
import RtvError from '../src/lib/RtvError';
describe('integration', function() {
describe('todos', function() {
let todo;
describe('Integration', function() {
describe('Typesets', function() {
it('should be valid non-fully-qualified typesets', function() {
rtv.isTypeset([rtv.t.BOOLEAN, [rtv.t.STRING], [rtv.t.INT]], {deep: true});
});
});
describe('Simple TODO items', function() {
let item;
let shape;
beforeEach(function() {
item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
note: {
text: 'Make 4 cups to have enough to share!',
updated: new Date('09/21/2018')
}
};
shape = {
title: rtv.t.STRING,
created: [rtv.q.OPTIONAL, rtv.t.DATE],
priority: [rtv.t.INT, {oneOf: [0, 1, 2]}],
note: [rtv.q.EXPECTED, {
text: rtv.t.STRING,
updated: rtv.t.DATE
}]
};
});
it('should validate', function() {
const result = rtv.check(item, shape);
expect(result).to.be.an.instanceof(RtvSuccess);
});
});
describe('Advanced TODO items', function() {
let item;
let shapes;
beforeEach(function() {
todo = {
item = {
title: 'Make Christmas Oatmeal',
due: new Date('12/25/2018'),
priority: 1,
......@@ -49,20 +86,113 @@ describe('integration', function() {
});
it('should validate', function() {
const result = rtv.check(todo, shapes.todo);
const result = rtv.check(item, shapes.todo);
expect(result).to.be.an.instanceof(RtvSuccess);
});
it('should fail if a note is missing "updated"', function() {
delete todo.notes[1].updated;
delete item.notes[1].updated;
const todoShape = shapes.todo;
const result = rtv.check(todo, todoShape);
const result = rtv.check(item, todoShape);
expect(result).to.be.an.instanceof(RtvError);
expect(result.value).to.equal(todo);
expect(result.value).to.equal(item);
expect(result.typeset).to.equal(todoShape);
expect(result.cause).to.eql(impl.fullyQualify(shapes.note.updated));
expect(result.cause).to.eql(rtv.fullyQualify(shapes.note.updated));
expect(result.path).to.eql(['notes', '1', 'updated']);
});
});
describe('Dynamic Note class', function() {
it('creates a class from a shape with setters that verify values', function() {
const {STRING, DATE} = rtv.t; // some types
const {EXPECTED} = rtv.q; // some qualifiers
const tags = ['car', 'money', 'reminder', 'grocery'];
const noteShape = {
// required, non-empty string
text: STRING,
// required array (could be empty) of non-empty tags names from the user's
// list of "tags"
tags: [[STRING, {oneOf: tags}]],
created: DATE, // required Date when the note was created
updated: [EXPECTED, DATE] // expected date of update (either null, or Date)
};
const classGenerator = function(shape) {
const ctor = function(initialValues) {
// by definition, a shape descriptor is made-up of its own-enumerable
// properties, so we enumerate them
const props = Object.keys(shape);
const typesets = {}; // prop -> fully-qualified array typeset
const values = {}; // prop -> value
let initializing = true; // true while we apply "initialValues"
props.forEach((prop) => {
typesets[prop] = rtv.fullyQualify(shape[prop]);
Object.defineProperty(this, prop, {
enumerable: true,
configurable: true, // could be false to lock this down further
get() {
return values[prop];
},
set(newValue) {
const typeset = typesets[prop].concat(); // shallow clone
if (initializing) {
// allow each property to be initially null, or as the typeset specifies
// so we don't end-up with junk data
// NOTE: in a fully-qualified typeset, the qualifier is always the
// first element
typeset[0] = EXPECTED;
}
// we assume there are no interdependencies between nested typesets
// this verification will throw an RtvError if the "newValue"
// violates the property's typeset
rtv.verify(newValue, typeset);
values[prop] = newValue;
}
});
if (initialValues && initialValues.hasOwnProperty(prop)) {
// go through the setter for verification
this[prop] = initialValues[prop];
} else {
// initialize to null
values[prop] = null;
}
});
initializing = false;
};
return ctor;
};
const Note = classGenerator(noteShape);
let note = new Note();
expect(Object.keys(note)).to.eql(Object.keys(noteShape));
expect(function() {
note.text = null;
}).to.throw(RtvError);
expect(function() {
note = new Note({
text: null,
tags: []
});
}).not.to.throw();
expect(note.text).to.equal(null);
expect(note.tags).to.eql([]);
expect(function() {
note.text = 'Awesome';
}).not.to.throw();
});
});
});
......@@ -530,6 +530,52 @@ describe('module: lib/impl', function() {
});
});
describe('#_callCustomValidator', function() {
it('should return undefined if the validator did not return anything', function() {
expect(impl._callCustomValidator(() => {}, 'foo', [], [])).to.equal(undefined);
});
it('should return undefined if the validator returned a truthy value', function() {
expect(impl._callCustomValidator(() => true, 'foo', [], [])).to.equal(undefined);
expect(impl._callCustomValidator(() => ({}), 'foo', [], [])).to.equal(undefined);
expect(impl._callCustomValidator(() => [], 'foo', [], [])).to.equal(undefined);
expect(impl._callCustomValidator(() => /hello/, 'foo', [], [])).to.equal(undefined);
expect(impl._callCustomValidator(() => 1, 'foo', [], [])).to.equal(undefined);
});
it('should return an Error if validator returned falsy value but not undefined', function() {
const failure = impl._callCustomValidator(() => false, 'foo', [], []);
expect(failure).to.be.an.instanceof(Error);
expect(failure.message).to.contain('Verification failed because of the custom validator');
expect(impl._callCustomValidator(() => null, 'foo', [], [])).to.be.an.instanceof(Error);
expect(impl._callCustomValidator(() => 0, 'foo', [], [])).to.be.an.instanceof(Error);
expect(impl._callCustomValidator(() => '', 'foo', [], [])).to.be.an.instanceof(Error);
});
it('should return what the customer validator threw', function() {
let err = new Error('bar');
const validator = function() {
throw err;
};
let failure = impl._callCustomValidator(validator, 'foo', [], []);
expect(failure).to.equal(err);
err = [];
failure = impl._callCustomValidator(validator, 'foo', [], []);
expect(failure).to.equal(err);
err = {};
failure = impl._callCustomValidator(validator, 'foo', [], []);
expect(failure).to.equal(err);
err = 'error';
failure = impl._callCustomValidator(validator, 'foo', [], []);
expect(failure).to.equal(err);
});
});
describe('#_getCheckOptions()', function() {
it('should return new default options if no current or override given', function() {
expect(impl._getCheckOptions()).to.eql({
......@@ -811,6 +857,74 @@ describe('module: lib/impl', function() {
expect(result.failure).to.equal(undefined);
});
it('should invoke all nested custom validators with respective values', function() {
const validator1 = sinon.spy();
const validator2 = sinon.spy();
const validator3 = sinon.spy();
const typeset = [{
prop1: [types.STRING, validator1],
prop2: [types.INT, validator2]
}, validator3];
const value = {
prop1: 'foo',
prop2: 77
};
const result = impl.checkWithArray(value, typeset);
expect(result).to.be.an.instanceof(RtvSuccess);
expect(validator1.callCount).to.equal(1);
expect(validator1.firstCall.args).to.eql([
'foo',
[qualifiers.REQUIRED, types.STRING],
[types.STRING, validator1]
]);
expect(validator2.callCount).to.equal(1);
expect(validator2.firstCall.args).to.eql([
77,
[qualifiers.REQUIRED, types.INT],
[types.INT, validator2]
]);
expect(validator3.callCount).to.equal(1);
expect(validator3.firstCall.args).to.eql([
value,
[qualifiers.REQUIRED, types.OBJECT, {$: {
prop1: typeset[0].prop1,
prop2: typeset[0].prop2
}}],
typeset
]);
});
it('should invoke a custom validator when value is null and qualifier is EXPECTED', function() {
const validator = sinon.stub().returns(true);
const typeset = [qualifiers.EXPECTED, types.STRING, validator];
let result = impl.checkWithArray(null, typeset);
expect(result).be.an.instanceof(RtvSuccess);
expect(validator.called).to.be.true;
expect(validator.firstCall.args).to.eql([
null,
[qualifiers.EXPECTED, types.STRING],
typeset
]);
validator.resetHistory();
typeset[0] = qualifiers.OPTIONAL;
result = impl.checkWithArray(undefined, typeset);
expect(result).be.an.instanceof(RtvSuccess);
expect(validator.called).to.be.true;
expect(validator.firstCall.args).to.eql([
undefined,
[qualifiers.OPTIONAL, types.STRING],
typeset
]);
});
describe('Options', function() {
it('should assume typeset is valid', function() {
const isTypesetStub = sinon.stub(isTypesetMod, 'default').callThrough();
......@@ -870,7 +984,7 @@ describe('module: lib/impl', function() {
expect(err.value).to.equal(value);
expect(err.path).to.eql([]);
expect(err.typeset).to.equal(typeset);
expect(err.cause).to.eql([qualifiers.REQUIRED, types.ANY]); // validator alone means ANY
expect(err.cause).to.eql([qualifiers.REQUIRED, types.ANY, typeset]); // validator alone means ANY
expect(err.failure).to.equal(cvError);
value = {foo: 'bar'};
......@@ -934,7 +1048,7 @@ describe('module: lib/impl', function() {
expect(result.value).to.equal('foo');
expect(result.path).to.eql([]);
expect(result.typeset).to.equal(validator);
expect(result.cause).to.eql([qualifiers.REQUIRED, types.ANY]);
expect(result.cause).to.eql([qualifiers.REQUIRED, types.ANY, validator]);
expect(result.failure).to.equal(cvError);
});
......
......@@ -132,7 +132,7 @@ describe('module: lib/validator/valHashMap', function() {
vtu.expectValidatorSuccess(val, map, undefined, {
keyExp: 'KEY\\d',
keyFlagSpec: 'i' // case-insensitive flag
keyFlags: 'i' // case-insensitive flag
});
let args = {
......@@ -145,7 +145,7 @@ describe('module: lib/validator/valHashMap', function() {
args = {
keyExp: 'KEY\\d',
keyFlagSpec: {} // ignored: not string (so still case-sensitive)
keyFlags: {} // ignored: not string (so still case-sensitive)
};
vtu.expectValidatorError(val, map, undefined, args, {
path: ['key="key1"'],
......
......@@ -369,6 +369,7 @@ export const expectValidatorError = function(validator, value, qualifier, args,
expect(result).to.be.an.instanceof(RtvError);
expect(result.valid).to.be.false;
expect(result.name).to.equal('RtvError');
// NOTE: Check for the existence of a property on `expectations` rather than
// a truthy value since `undefined`, `null`, `0`, `false`, or an empty string
......
......@@ -140,13 +140,13 @@ describe('module: lib/validator/valMap', function() {
vtu.expectValidatorSuccess(val, map, undefined, {
keys: [qualifiers.EXPECTED, types.STRING],
keyExp: 'KEY\\d',
keyFlagSpec: 'i' // case-insensitive flag
keyFlags: 'i' // case-insensitive flag
});
args = {
keys: [qualifiers.EXPECTED, types.STRING],
keyExp: 'KEY\\d',
keyFlagSpec: {} // ignored: not string (so still case-sensitive)
keyFlags: {} // ignored: not string (so still case-sensitive)
};
vtu.expectValidatorError(val, map, undefined, args, {
path: ['key="key1"'],
......
......@@ -79,11 +79,19 @@ describe('module: lib/validator/valString', function() {
it('checks for an exact value', function() {
vtu.expectValidatorSuccess(val, 'foo', undefined, {oneOf: 'foo'});
vtu.expectValidatorError(val, 'bar', undefined, {oneOf: 'foo'});
});
// empty string is OK with the right qualifier
vtu.expectValidatorError(val, '', undefined, {oneOf: ''});
it('empty string is OK if oneOf allows it', function() {
vtu.expectValidatorSuccess(val, '', undefined, {oneOf: ''});
vtu.expectValidatorSuccess(val, '', qualifiers.EXPECTED, {oneOf: ''});
vtu.expectValidatorSuccess(val, '', qualifiers.OPTIONAL, {oneOf: ''});
// null is not considered empty
vtu.expectValidatorSuccess(val, null, qualifiers.EXPECTED, {oneOf: ''});
vtu.expectValidatorSuccess(val, '', undefined, {oneOf: ['']});
vtu.expectValidatorSuccess(val, '', qualifiers.EXPECTED, {oneOf: ['']});
vtu.expectValidatorSuccess(val, '', qualifiers.OPTIONAL, {oneOf: ['']});
});
it('checks for an exact string in a list', function() {
......@@ -92,11 +100,6 @@ describe('module: lib/validator/valString', function() {
vtu.expectValidatorSuccess(val, '7', undefined, {oneOf: ['7']});
vtu.expectValidatorSuccess(val, '7', undefined, {oneOf: []}); // ignored
// if qualifier allows empty string as a value, empty string works in a list
vtu.expectValidatorError(val, '', undefined, {oneOf: ['']});
vtu.expectValidatorError(val, '', qualifiers.EXPECTED, {oneOf: ['']});
vtu.expectValidatorError(val, '', qualifiers.OPTIONAL, {oneOf: ['']});
// ignores non-type values in a list
vtu.expectValidatorError(val, '7', undefined, {oneOf: [null, 7, true]});
......@@ -125,10 +128,15 @@ describe('module: lib/validator/valString', function() {
vtu.expectValidatorSuccess(val, 'minimum', undefined, {min: NaN});
vtu.expectValidatorSuccess(val, 'minimum', undefined, {min: Infinity});
vtu.expectValidatorSuccess(val, 'minimum', undefined, {min: -Infinity});
});
// default qualifier requires non-empty
vtu.expectValidatorError(val, '', undefined, {min: 0});
it('empty string is allowed if min requires it', function() {
vtu.expectValidatorSuccess(val, '', undefined, {min: 0});
vtu.expectValidatorSuccess(val, '', qualifiers.EXPECTED, {min: 0});
vtu.expectValidatorSuccess(val, '', qualifiers.OPTIONAL, {min: 0});
// null is OK because of EXPECTED
vtu.expectValidatorSuccess(val, null, qualifiers.EXPECTED, {min: 1});
});
it('min takes precedence over partial', function() {
......@@ -138,11 +146,6 @@ describe('module: lib/validator/valString', function() {
// min is ignored (less than 0) to partial wins, but there's no match
vtu.expectValidatorError(val, 'minimum', undefined, {min: -100, partial: 'foo'});
vtu.expectValidatorSuccess(val, 'minimum', undefined, {min: -1, partial: 'nim'});
// default qualifier requires non-empty
vtu.expectValidatorError(val, '', undefined, {min: 0, partial: ''});
vtu.expectValidatorSuccess(val, '', qualifiers.OPTIONAL, {min: 0, partial: ''});
vtu.expectValidatorSuccess(val, 'foo', undefined, {min: 0, partial: ''});
});
it('checks for max length if "exact" is not specified', function() {
......@@ -155,10 +158,15 @@ describe('module: lib/validator/valString', function() {
vtu.expectValidatorSuccess(val, 'maximum', undefined, {max: NaN});
vtu.expectValidatorSuccess(val, 'maximum', undefined, {max: Infinity});
vtu.expectValidatorSuccess(val, 'maximum', undefined, {max: -Infinity});
});
// default qualifier requires non-empty
vtu.expectValidatorError(val, '', undefined, {max: 0});
it('empty string is allowed if max requires it', function() {
vtu.expectValidatorSuccess(val, '', undefined, {max: 0});
vtu.expectValidatorSuccess(val, '', qualifiers.EXPECTED, {max: 0});
vtu.expectValidatorSuccess(val, '', qualifiers.OPTIONAL, {max: 0});
// null is OK because of EXPECTED
vtu.expectValidatorSuccess(val, null, qualifiers.EXPECTED, {max: 0});
});
it('max takes precedence over partial', function() {
......@@ -168,12 +176,6 @@ describe('module: lib/validator/valString', function() {
vtu.expectValidatorError(val, 'maximum', undefined, {max: -100, partial: 'foo'});
vtu.expectValidatorSuccess(val, 'maximum', undefined, {max: -1, partial: 'xim'});