432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
/**
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const root = require( '../../' );
|
|
const validate = root.validate;
|
|
const Sut = validate.DataValidator;
|
|
const MemoryStore = root.store.MemoryStore;
|
|
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 { sut, vmonitor, dep_factory, getStore } = createStubs( {
|
|
bvalidator: bvalidator,
|
|
} );
|
|
|
|
const { bstore } = getStore();
|
|
|
|
const mock_bstore = sinon.mock( bstore );
|
|
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 }
|
|
};
|
|
|
|
// call to actual validator
|
|
mock_vmonitor.expects( 'update' )
|
|
.once()
|
|
.withExactArgs( getStore().store, expected_failures )
|
|
.returns( Promise.resolve( undefined ) );
|
|
|
|
mock_dep_factory.expects( 'createFieldFailure' )
|
|
.once()
|
|
.withExactArgs( 'foo', 1, expected_value )
|
|
.returns( expected_failure );
|
|
|
|
// clears previous diffs
|
|
mock_bstore.expects( 'clear' )
|
|
.once()
|
|
.returns( Promise.resolve( bstore ) );
|
|
|
|
return Sut( bvalidator, vmonitor, dep_factory, getStore )
|
|
.validate( diff )
|
|
.then( () =>
|
|
{
|
|
mock_vmonitor.verify();
|
|
mock_dep_factory.verify();
|
|
|
|
// cleared on call to err in above mock validator
|
|
return expect( getStore().bstore.get( 'foo' ) )
|
|
.to.eventually.deep.equal( [ 'a', undefined, 'c' ] );
|
|
} );
|
|
} );
|
|
|
|
|
|
it( 'merges classification changes with diff', () =>
|
|
{
|
|
// SUT will only care about the indexes
|
|
const classes = {
|
|
first: { indexes: [], is: false },
|
|
second: { indexes: [ 0, 1 ], is: true },
|
|
};
|
|
|
|
const { sut, getStore } = createStubs( {} );
|
|
const { cstore } = getStore();
|
|
|
|
const mock_cstore = sinon.mock( cstore );
|
|
|
|
// clears previous diffs
|
|
mock_cstore.expects( 'clear' )
|
|
.once()
|
|
.returns( Promise.resolve( cstore ) );
|
|
|
|
return sut
|
|
.validate( {}, classes )
|
|
.then( () =>
|
|
{
|
|
// clear should have been called
|
|
mock_cstore.verify();
|
|
|
|
// keep in mind that we are using MemoryStore for this
|
|
// test (whereas a real implementation would probably be
|
|
// using a DiffStore)
|
|
return Promise.all(
|
|
Object.keys( classes ).map( key =>
|
|
expect( cstore.get( key ) )
|
|
.to.eventually.deep.equal( classes[ key ].indexes )
|
|
)
|
|
);
|
|
} );
|
|
} );
|
|
|
|
|
|
it( 'considers failures from external validator', () =>
|
|
{
|
|
const expected_failure = {};
|
|
|
|
const bvalidator = createMockBucketValidator(
|
|
function( data, err, _ )
|
|
{
|
|
// see `failures` below
|
|
err( 'foo', 'moo', 2 );
|
|
}
|
|
);
|
|
|
|
const { sut, vmonitor, dep_factory, getStore } = createStubs( {
|
|
bvalidator: bvalidator,
|
|
} );
|
|
|
|
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( getStore().store, expected_failures )
|
|
.returns( Promise.resolve( undefined ) );
|
|
|
|
sinon.mock( dep_factory )
|
|
.expects( 'createFieldFailure' )
|
|
.returns( expected_failure );
|
|
|
|
return Sut( bvalidator, vmonitor, dep_factory, getStore )
|
|
.validate( diff, {}, validatef );
|
|
} );
|
|
|
|
|
|
it( 'rejects if field monitor update rejects', () =>
|
|
{
|
|
const { sut, vmonitor } = createStubs( {} );
|
|
|
|
const expected_e = Error();
|
|
|
|
sinon.mock( vmonitor )
|
|
.expects( 'update' )
|
|
.once()
|
|
.returns( Promise.reject( expected_e ) );
|
|
|
|
return expect( sut.validate( {} ) ).
|
|
to.eventually.be.rejectedWith( expected_e );
|
|
} );
|
|
|
|
|
|
// otherwise system might get into an unexpected state
|
|
it( 'queues concurrent validations', () =>
|
|
{
|
|
const expected_failure = {};
|
|
|
|
let vcalled = 0;
|
|
|
|
const bvalidator = createMockBucketValidator(
|
|
( _, __, ___ ) => vcalled++
|
|
);
|
|
|
|
const vmonitor = sinon.createStubInstance( ValidStateMonitor );
|
|
|
|
const { sut, getStore } = createStubs( {
|
|
bvalidator: bvalidator,
|
|
vmonitor: vmonitor,
|
|
} );
|
|
|
|
const diff_a = { foo: [ 'a', 'b', 'c' ] };
|
|
const diff_b = { foo: [ 'd' ] };
|
|
|
|
const validatef = ( diff, failures ) =>
|
|
{
|
|
// not a real failure; just used to transfer state to stub
|
|
// (see below)
|
|
failures.failedon = diff;
|
|
};
|
|
|
|
return new Promise( ( accept, reject ) =>
|
|
{
|
|
// by the time it gets to this the second time, store could
|
|
// be in any sort of state depending on what callbacks were
|
|
// invoked first (implementation details)
|
|
vmonitor.update = ( _, failures ) =>
|
|
{
|
|
const orig_diff = failures.failedon;
|
|
|
|
// if the external validator was called twice, then they
|
|
// didn't wait for us to finish
|
|
if ( ( orig_diff === diff_a ) && ( vcalled !== 1 ) )
|
|
{
|
|
reject( Error( "Request not queued" ) );
|
|
}
|
|
|
|
// if this key doesn't exist, then the store has been
|
|
// cleared (which happens before it's re-populated with
|
|
// the new diff)
|
|
return expect( getStore().bstore.get( 'foo' ) )
|
|
.to.eventually.deep.equal( orig_diff.foo )
|
|
.then( () => {
|
|
// the second test, after which we're done
|
|
if ( orig_diff === diff_b )
|
|
{
|
|
accept();
|
|
}
|
|
} )
|
|
.catch( e => reject( e ) );
|
|
};
|
|
|
|
sut.validate( diff_a, {}, validatef );
|
|
sut.validate( diff_b, {}, validatef );
|
|
} );
|
|
} );
|
|
} );
|
|
|
|
|
|
describe( '#updateFailures', () =>
|
|
{
|
|
it( 'directly updates failures', () =>
|
|
{
|
|
const { sut, vmonitor, getStore } = createStubs( {} );
|
|
const { bstore } = getStore();
|
|
|
|
const diff = {
|
|
foo: [ 'bar' ],
|
|
bar: [ 'baz' ],
|
|
};
|
|
|
|
const failures = {};
|
|
|
|
const mock_vmonitor = sinon.mock( vmonitor );
|
|
const mock_bstore = sinon.mock( bstore );
|
|
|
|
// clears previous diffs
|
|
mock_bstore.expects( 'clear' )
|
|
.once()
|
|
.returns( Promise.resolve( bstore) );
|
|
|
|
mock_vmonitor
|
|
.expects( 'update' )
|
|
.once()
|
|
.withExactArgs( getStore().store, failures );
|
|
|
|
return sut
|
|
.updateFailures( diff, failures )
|
|
.then( () =>
|
|
{
|
|
mock_vmonitor.verify();
|
|
mock_bstore.verify();
|
|
|
|
// keep in mind that we are using MemoryStore for this
|
|
// test (whereas a real implementation would probably be
|
|
// using a DiffStore)
|
|
return Promise.all(
|
|
Object.keys( diff ).map( key =>
|
|
expect( bstore.get( key ) )
|
|
.to.eventually.deep.equal( diff[ key ] )
|
|
)
|
|
);
|
|
} );
|
|
} );
|
|
|
|
|
|
it( 'queues concurrent requests', () =>
|
|
{
|
|
const { sut, vmonitor, getStore } = createStubs( {
|
|
vmonitor: sinon.createStubInstance( ValidStateMonitor ),
|
|
} );
|
|
const { bstore } = getStore();
|
|
|
|
// linked list (to previous)
|
|
const fails = [ {}, {}, {} ];
|
|
fails.forEach( ( fail, i ) => fail.prev = fails[ i - 1 ] );
|
|
|
|
const failcalls = [];
|
|
|
|
vmonitor.update = ( _, fail ) =>
|
|
{
|
|
failcalls.push( fail );
|
|
return Promise.resolve( true );
|
|
};
|
|
|
|
return Promise.all( [
|
|
sut.updateFailures( {}, fails[ 0 ] )
|
|
.then( () => expect( failcalls[ 0 ] ).to.equal( fails[ 0 ] ) ),
|
|
|
|
sut.updateFailures( {}, fails[ 1 ] )
|
|
.then( () => expect( failcalls[ 1 ] ).to.equal( fails[ 1 ] ) ),
|
|
|
|
sut.updateFailures( {}, fails[ 2 ] )
|
|
.then( () => expect( failcalls[ 2 ] ).to.equal( fails[ 2 ] ) ),
|
|
] )
|
|
.then( () => {
|
|
// sanity check to make sure the above stuff was
|
|
// actually called
|
|
expect( failcalls.length )
|
|
.to.equal( fails.length )
|
|
} );
|
|
} );
|
|
} );
|
|
|
|
|
|
describe( '#clearFailures', () =>
|
|
{
|
|
it( 'proxies to validator', () =>
|
|
{
|
|
const { sut, vmonitor } = createStubs( {} );
|
|
const mock_vmonitor = sinon.mock( vmonitor );
|
|
|
|
const failures = [ 'foo', 'bar' ];
|
|
|
|
mock_vmonitor
|
|
.expects( 'clearFailures' )
|
|
.once()
|
|
.withExactArgs( failures );
|
|
|
|
expect( sut.clearFailures( failures ) )
|
|
.to.equal( sut );
|
|
|
|
mock_vmonitor.verify();
|
|
} );
|
|
} );
|
|
} );
|
|
|
|
|
|
function createMockBucketValidator( validatef )
|
|
{
|
|
validatef = validatef || ( ( x, y, z ) => {} );
|
|
|
|
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: () => {},
|
|
};
|
|
}
|
|
|
|
|
|
function createStubStore()
|
|
{
|
|
const stores = {
|
|
store: MemoryStore(),
|
|
bstore: MemoryStore(),
|
|
cstore: MemoryStore(),
|
|
};
|
|
|
|
return () => stores;
|
|
}
|
|
|
|
|
|
function createStubs( {
|
|
bvalidator = createMockBucketValidator(),
|
|
vmonitor = ValidStateMonitor(),
|
|
dep_factory = createMockDependencyFactory(),
|
|
getStore = createStubStore(),
|
|
} )
|
|
{
|
|
return {
|
|
bvalidator: bvalidator,
|
|
vmonitor: vmonitor,
|
|
dep_factory: dep_factory,
|
|
getStore: getStore,
|
|
sut: Sut( bvalidator, vmonitor, dep_factory, getStore ),
|
|
};
|
|
}
|