diff --git a/src/validate/DataValidator.js b/src/validate/DataValidator.js new file mode 100644 index 0000000..0fead8d --- /dev/null +++ b/src/validate/DataValidator.js @@ -0,0 +1,105 @@ +/** + * Data validator + * + * Copyright (C) 2017 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +"use strict"; + +const Class = require( 'easejs' ).Class; + + +/** + * Check data update for failures + * + * This validator glues together various parts of the system that contribute + * to a validation on data change. + * + * TODO: Remove reliance on ClientDependencyFactory + */ +module.exports = Class( 'DataValidator', +{ + /** + * Bucket data validator + * @type {BucketDataValidator} + */ + 'private _bucket_validator': null, + + /** + * Bucket field monitor + * @type {ValidStateMonitor} + */ + 'private _field_monitor': null, + + /** + * Dependency factory + * + * TODO: remove dependency on this class + * + * @type {ClientDependencyFactory} + */ + 'private _factory': null, + + + /** + * Initialize validator + * + * @param {BucketDataValidator} bucket_validator data validator + * @param {ValidStateMonitor} field_monitor field state monitor + * @param {ClientDependencyFactory} dep_factory REMOVE ME + */ + __construct( bucket_validator, field_monitor, dep_factory ) + { + this._bucket_validator = bucket_validator; + this._field_monitor = field_monitor; + this._factory = dep_factory; + }, + + + /** + * Validate diff and update field monitor + * + * The external validator `validatef` is a kluge while the system + * undergoes refactoring. + * + * @param {Object} diff bucket diff + * @param {function(Object,Object)=} validatef external validator + * + * @return {Promise} accepts with unspecified value once field monitor + * has completed its update + */ + 'public validate'( diff, validatef ) + { + const _self = this; + + let failures = {}; + + this._bucket_validator.validate( diff, ( name, value, i ) => + { + diff[ name ][ i ] = undefined; + + ( failures[ name ] = failures[ name ] || {} )[ i ] = + _self._factory.createFieldFailure( name, i, value ); + }, true ); + + validatef && validatef( diff, failures ); + + // XXX: this assumes that the above is synchronous + return this._field_monitor.update( diff, failures ); + }, +} ); diff --git a/test/validate/DataValidatorTest.js b/test/validate/DataValidatorTest.js new file mode 100644 index 0000000..a209486 --- /dev/null +++ b/test/validate/DataValidatorTest.js @@ -0,0 +1,183 @@ +/** + * Test data validator + * + * Copyright (C) 2017 LoVullo Associates, Inc. + * + * This file is part of liza. + * + * liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +"use strict"; + +const root = require( '../../' ); +const validate = root.validate; +const Sut = validate.DataValidator; +const chai = require( 'chai' ); +const expect = chai.expect; +const sinon = require( 'sinon' ); + +const BucketDataValidator = validate.BucketDataValidator, + ValidStateMonitor = validate.ValidStateMonitor; + +chai.use( require( 'chai-as-promised' ) ); + + +describe( 'DataValidator', () => +{ + describe( '#validate', () => + { + it( 'validates against bucket validator', () => + { + const bvalidator = createMockBucketValidator( + function( data, err, inplace ) + { + expect( data ).to.equal( diff ); + expect( inplace ).to.be.true; + + // since we're mocking #validate, the callback will not + // be called; we'll have to do so ourselves (eventually + // this will be a promise) + err( 'foo', expected_value, 1 ); + } + ); + + const vmonitor = ValidStateMonitor(); + const dep_factory = createMockDependencyFactory(); + + const mock_vmonitor = sinon.mock( vmonitor ); + const mock_dep_factory = sinon.mock( dep_factory ); + + const diff = { foo: [ 'a', 'b', 'c' ] }; + const expected_failure = {}; + const expected_value = 'errmsg'; + const expected_failures = { + foo: { 1: expected_failure } + }; + + mock_vmonitor.expects( 'update' ) + .once() + .withExactArgs( diff, expected_failures ) + .returns( Promise.resolve( undefined ) ); + + mock_dep_factory.expects( 'createFieldFailure' ) + .once() + .withExactArgs( 'foo', 1, expected_value ) + .returns( expected_failure ); + + const retp = Sut( bvalidator, vmonitor, dep_factory ) + .validate( diff ); + + // cleared on call to err in above mock validator + expect( diff.foo ).to.deep.equal( + [ 'a', undefined, 'c' ] + ); + + mock_vmonitor.verify(); + mock_dep_factory.verify(); + + // the promise + return retp; + } ); + + + it( 'considers failures from external validator', () => + { + const expected_failure = {}; + + const bvalidator = createMockBucketValidator( + function( data, err, _ ) + { + // see `failures` below + err( 'foo', 'moo', 2 ); + } + ); + + const vmonitor = ValidStateMonitor(); + const dep_factory = createMockDependencyFactory(); + + const diff = { foo: [ 'a', 'b', 'c' ] }; + const expected_failures = { + foo: { + 0: expected_failure, + 2: expected_failure, + }, + }; + + const validatef = ( given_diff, given_failures ) => + { + expect( given_diff ).to.equal( diff ); + expect( given_failures.foo[ 2 ] ) + .to.equal( expected_failure ); + + given_failures.foo[ 0 ] = expected_failure; + }; + + // TODO: this is an implementation detail left over from the + // good 'ol days; remove it + sinon.mock( vmonitor ) + .expects( 'update' ) + .once() + .withExactArgs( diff, expected_failures ) + .returns( Promise.resolve( undefined ) ); + + sinon.mock( dep_factory ) + .expects( 'createFieldFailure' ) + .returns( expected_failure ); + + return Sut( bvalidator, vmonitor, dep_factory ) + .validate( diff, validatef ); + } ); + + + it( 'rejects if field monitor update rejects', () => + { + const bvalidator = createMockBucketValidator( ( x, y, z ) => {} ); + const vmonitor = ValidStateMonitor(); + const dep_factory = createMockDependencyFactory(); + + const expected_e = Error(); + + sinon.mock( vmonitor ) + .expects( 'update' ) + .once() + .returns( Promise.reject( expected_e ) ); + + return expect( + Sut( bvalidator, vmonitor, dep_factory ) + .validate( {} ) + ).to.eventually.be.rejectedWith( expected_e ); + } ); + } ); +} ); + + +function createMockBucketValidator( validatef ) +{ + return BucketDataValidator.extend( + { + 'override public validate': validatef, + } )(); +} + + +// This isn't yet moved into liza (at least at the time of writing this) +function createMockDependencyFactory( map ) +{ + // alternative to mocking since the ClientDependencyFactory is not going + // to be used in the future + return { + createFieldFailure: () => {}, + }; +}