diff --git a/README.md b/README.md index dcb4ae5..12ba5d2 100644 --- a/README.md +++ b/README.md @@ -328,20 +328,21 @@ Your validator must implement the following methods. ```node class CustomValidator { /** - * Validates an event. + * Validate an event. * * @param {EventInterface} event - * @return {Promise} + * @return {Promise} * @example - * validator.validates(event).then((success) => { - * if (success) { + * validator.validate(event).then((result) => { + * if (result.passes) { * console.log('event validates!'); * } else { * console.log('event failed validation'); + * console.log(result.errors); * } * }); */ - validates(event) { + validate(event) { } } @@ -418,8 +419,9 @@ manager.listenExprFailHandler = (event, expr) => { }; // hook into validation failures -manager.validationFailHandler = (event, validator) => { +manager.validationFailHandler = (result) => { // the event failed validation + console.log(result.errors); }; ``` diff --git a/changelog.md b/changelog.md index 9d63fb3..8984949 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 3.0.0 - 2017-07-18 + +* EventValidatorInterface->validates() renamed to ->validate() +* EventValidatorInterface->validate() now returns a promise resolving to a ValidationResult instance instead of bool +* The validation fail handler callback now receives a ValidationResult instead of the event and a validator +* dispatch() now returns a promise +* dispatchBatch() now returns a promise +* Events are now validated on dispatch, and will reject the promise with a ValidationResult when validation fails + ## 2.0.1 - 2017-07-18 * Add support for translate, listen expr & validation failure callbacks diff --git a/package.json b/package.json index 3820c44..8acae14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@superbalist/js-event-pubsub", - "version": "2.0.1", + "version": "3.0.0", "description": "An event protocol and implementation over pub/sub", "main": "lib/index.js", "scripts": { @@ -27,7 +27,7 @@ }, "homepage": "https://github.com/Superbalist/js-event-pubsub#readme", "dependencies": { - "@superbalist/js-pubsub": "^2.0.0", + "@superbalist/js-pubsub": "^3.0.0", "ajv": "^5.0.1", "request": "^2.81.0", "request-promise-native": "^1.0.3", diff --git a/src/EventManager.js b/src/EventManager.js index a8c4af6..7578583 100644 --- a/src/EventManager.js +++ b/src/EventManager.js @@ -148,14 +148,14 @@ class EventManager { // not using a validator handler(event); } else { - this.validator.validates(event).then((success) => { - if (success) { + this.validator.validate(event).then((result) => { + if (result.passes) { // event passed validation handler(event); } else { // pass to validation fail handler? if (this.validationFailHandler) { - this.validationFailHandler(event, this.validator); + this.validationFailHandler(result); } } }); @@ -220,6 +220,7 @@ class EventManager { * * @param {string} channel * @param {EventInterface} event + * @return {Promise<*>} * @example * let event = new SimpleEvent('user.created', { * user: { @@ -233,8 +234,30 @@ class EventManager { * manager->dispatch('events', event); */ dispatch(channel, event) { - event = this._prepEventForDispatch(event); - this.adapter.publish(channel, event.toMessage()); + return new Promise((resolve, reject) => { + event = this._prepEventForDispatch(event); + + let publishAndResolve = () => { + return this.adapter.publish(channel, event.toMessage()).then(resolve); + }; + + if (this.validator) { + this.validator.validate(event).then((result) => { + if (result.passes) { + publishAndResolve(); + } else { + // pass to validation fail handler? + if (this.validationFailHandler) { + this.validationFailHandler(result); + } + + reject(result); + } + }); + } else { + publishAndResolve(); + } + }); } /** @@ -242,6 +265,7 @@ class EventManager { * * @param {string} channel * @param {EventInterface[]} events + * @return {Promise<*>} * @example * let events = [ * new SimpleEvent('user.created', { @@ -265,11 +289,46 @@ class EventManager { * manager->dispatchBatch('events', events); */ dispatchBatch(channel, events) { - let messages = events.map((event) => { - event = this._prepEventForDispatch(event); - return event.toMessage(); + return new Promise((resolve, reject) => { + let validators = []; + let messages = []; + + for (let event of events) { + event = this._prepEventForDispatch(event); + + messages.push(event.toMessage()); + + if (this.validator) { + validators.push(this.validator.validate(event)); + } + } + + Promise.all(validators).then((results) => { + let validates = true; + let firstFailedResult = null; + + for (let result of results) { + if (result.fails) { + // when we hit the first event that fails validation, + // we'll break out of here and reject the promise + firstFailedResult = result; + validates = false; + break; + } + } + + if (validates) { + this.adapter.publishBatch(channel, messages).then(resolve); + } else { + // pass to validation fail handler? + if (this.validationFailHandler) { + this.validationFailHandler(firstFailedResult); + } + + reject(firstFailedResult); + } + }); }); - this.adapter.publishBatch(channel, messages); } } diff --git a/src/EventValidatorInterface.js b/src/EventValidatorInterface.js index 7c027f6..951a6f1 100644 --- a/src/EventValidatorInterface.js +++ b/src/EventValidatorInterface.js @@ -8,20 +8,21 @@ */ class EventValidatorInterface { /** - * Validates an event. + * Validate an event. * * @param {EventInterface} event - * @return {Promise} + * @return {Promise} * @example - * validator.validates(event).then((success) => { - * if (success) { + * validator.validate(event).then((result) => { + * if (result.passes) { * console.log('event validates!'); * } else { * console.log('event failed validation'); + * console.log(result.errors); * } * }); */ - validates(event) { + validate(event) { } } diff --git a/src/ValidationResult.js b/src/ValidationResult.js new file mode 100644 index 0000000..7da4f60 --- /dev/null +++ b/src/ValidationResult.js @@ -0,0 +1,43 @@ +'use strict'; + +/** + * ValidationResult Class + */ +class ValidationResult { + /** + * Construct an ValidationResult + * + * @param {EventValidatorInterface} validator + * @param {EventInterface} event + * @param {boolean} passes + * @param {string[]} [errors=[]] + */ + constructor(validator, event, passes, errors = []) { + /** + * @type {EventValidatorInterface} + */ + this.validator = validator; + + /** + * @type {EventInterface} + */ + this.event = event; + + /** + * @type {boolean} + */ + this.passes = passes; + + /** + * @type {boolean} + */ + this.fails = !passes; + + /** + * @type {string[]} + */ + this.errors = errors; + } +} + +module.exports = ValidationResult; diff --git a/src/validators/JSONSchemaEventValidator.js b/src/validators/JSONSchemaEventValidator.js index 522f2c2..e134aad 100644 --- a/src/validators/JSONSchemaEventValidator.js +++ b/src/validators/JSONSchemaEventValidator.js @@ -2,6 +2,7 @@ let Ajv = require('ajv'); let request = require('request-promise-native'); +let ValidationResult = require('../ValidationResult'); /** * JSONSchemaEventValidator Class @@ -22,23 +23,33 @@ class JSONSchemaEventValidator { } /** - * Validates an event. + * Validate an event. * * @param {SchemaEvent} event - * @return {Promise} + * @return {Promise} * @example - * validator.validates(event).then((success) => { - * if (success) { + * validator.validate(event).then((result) => { + * if (result.passes) { * console.log('event validates!'); * } else { * console.log('event failed validation'); + * console.log(result.errors); * } * }); */ - validates(event) { + validate(event) { + let _this = this; + return this.ajv.compileAsync({$ref: event.schema}) .then(function(validate) { - return validate(event.toMessage()); + if (validate(event.toMessage())) { + return new ValidationResult(_this, event, true); + } else { + let errors = validate.errors.map((error) => { + return error.message; + }); + return new ValidationResult(_this, event, false, errors); + } } ); } diff --git a/test/EventManagerTest.js b/test/EventManagerTest.js index a9ed8c0..3dce0c0 100644 --- a/test/EventManagerTest.js +++ b/test/EventManagerTest.js @@ -10,6 +10,7 @@ let EventValidatorInterface = require('../lib/EventValidatorInterface'); let EventManager = require('../lib/EventManager'); let EventInterface = require('../lib/EventInterface'); let SimpleEvent = require('../lib/events/SimpleEvent'); +let ValidationResult = require('../lib/ValidationResult'); describe('EventManager', () => { describe('construct instance', () => { @@ -205,7 +206,7 @@ describe('EventManager', () => { .returns(event); let validator = sinon.createStubInstance(EventValidatorInterface); - validator.validates = sinon.stub() + validator.validate = sinon.stub() .returns(new Promise((resolve, reject) => {})); let manager = new EventManager(adapter, translator, validator); @@ -216,8 +217,8 @@ describe('EventManager', () => { adapter.subscribe.yield({'event': 'user.created'}); - sinon.assert.calledOnce(validator.validates); - sinon.assert.calledWith(validator.validates, event); + sinon.assert.calledOnce(validator.validate); + sinon.assert.calledWith(validator.validate, event); }); it('when a message is received & the translator fails, the handler should not be called', () => { @@ -350,12 +351,15 @@ describe('EventManager', () => { translator.translate = sinon.stub() .returns(event); + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult = new ValidationResult(validator, event, true); + let validationPromise = new Promise((resolve, reject) => { - resolve(true); + resolve(validationResult); }); - let validator = sinon.createStubInstance(EventValidatorInterface); - validator.validates = sinon.stub() + validator.validate = sinon.stub() .returns(validationPromise); let manager = new EventManager(adapter, translator, validator); @@ -384,12 +388,15 @@ describe('EventManager', () => { translator.translate = sinon.stub() .returns(event); + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult = new ValidationResult(validator, event, false, ['should have required property \'user\'']); + let validationPromise = new Promise((resolve, reject) => { - resolve(false); + resolve(validationResult); }); - let validator = sinon.createStubInstance(EventValidatorInterface); - validator.validates = sinon.stub() + validator.validate = sinon.stub() .returns(validationPromise); let manager = new EventManager(adapter, translator, validator); @@ -418,12 +425,15 @@ describe('EventManager', () => { translator.translate = sinon.stub() .returns(event); + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult = new ValidationResult(validator, event, false, ['should have required property \'user\'']); + let validationPromise = new Promise((resolve, reject) => { - resolve(false); + resolve(validationResult); }); - let validator = sinon.createStubInstance(EventValidatorInterface); - validator.validates = sinon.stub() + validator.validate = sinon.stub() .returns(validationPromise); let validationFailHandler = sinon.spy(); @@ -441,14 +451,15 @@ describe('EventManager', () => { sinon.assert.notCalled(handler); sinon.assert.calledOnce(validationFailHandler); - sinon.assert.calledWith(validationFailHandler, event, validator); + sinon.assert.calledWith(validationFailHandler, validationResult); }); }); describe('dispatch', () => { it('should convert the event to a message and publish to the channel', () => { let adapter = sinon.createStubInstance(PubSubAdapterInterface); - adapter.publish = sinon.stub(); + adapter.publish = sinon.stub() + .returns(Promise.resolve('result')); let translator = sinon.createStubInstance(MessageTranslatorInterface); let manager = new EventManager(adapter, translator); @@ -457,17 +468,20 @@ describe('EventManager', () => { event.toMessage = sinon.stub() .returns({'event': 'user.created'}); - manager.dispatch('my_channel', event); + return manager.dispatch('my_channel', event).then((result) => { + sinon.assert.calledOnce(event.toMessage); - sinon.assert.calledOnce(event.toMessage); + sinon.assert.calledOnce(adapter.publish); + sinon.assert.calledWith(adapter.publish, 'my_channel', {'event': 'user.created'}); - sinon.assert.calledOnce(adapter.publish); - sinon.assert.calledWith(adapter.publish, 'my_channel', {'event': 'user.created'}); + expect(result).to.equal('result'); + }); }); it('should automagically inject values from attribute injectors into the message payload', () => { let adapter = sinon.createStubInstance(PubSubAdapterInterface); - adapter.publish = sinon.stub(); + adapter.publish = sinon.stub() + .returns(Promise.resolve('result')); let translator = sinon.createStubInstance(MessageTranslatorInterface); let manager = new EventManager(adapter, translator); @@ -477,23 +491,110 @@ describe('EventManager', () => { let event = new SimpleEvent('user.created'); - manager.dispatch('my_channel', event); - - sinon.assert.calledOnce(adapter.publish); - sinon.assert.calledWith( - adapter.publish, - 'my_channel', - { - 'event': 'user.created', - 'service': 'search', - 'hello': 'world', - } - ); + return manager.dispatch('my_channel', event).then((result) => { + sinon.assert.calledOnce(adapter.publish); + sinon.assert.calledWith( + adapter.publish, + 'my_channel', + { + 'event': 'user.created', + 'service': 'search', + 'hello': 'world', + } + ); + + expect(result).to.equal('result'); + }); + }); + + it('should convert the event to a message and publish to the channel, when a validator is set and validation passes', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + adapter.publish = sinon.stub() + .returns(Promise.resolve('result')); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event = sinon.createStubInstance(EventInterface); + event.toMessage = sinon.stub() + .returns({'event': 'user.created'}); + + let validator = sinon.createStubInstance(EventValidatorInterface); + validator.validate = sinon.stub() + .returns(Promise.resolve(new ValidationResult(validator, event, true))); + + let manager = new EventManager(adapter, translator, validator); + + return manager.dispatch('my_channel', event).then((result) => { + sinon.assert.calledOnce(event.toMessage); + + sinon.assert.calledOnce(validator.validate); + sinon.assert.calledWith(validator.validate, event); + + sinon.assert.calledOnce(adapter.publish); + sinon.assert.calledWith(adapter.publish, 'my_channel', {'event': 'user.created'}); + + expect(result).to.equal('result'); + }); + }); + + it('should reject the promise when a validator is set and validation fails', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event = sinon.createStubInstance(EventInterface); + + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult = new ValidationResult(validator, event, false, ['should have required property \'user\'']); + + validator.validate = sinon.stub() + .returns(Promise.resolve(validationResult)); + + let manager = new EventManager(adapter, translator, validator); + + return manager.dispatch('my_channel', event).catch((reason) => { + sinon.assert.calledOnce(validator.validate); + sinon.assert.calledWith(validator.validate, event); + + expect(reason).to.equal(validationResult); + }); + }); + + it('should call the validationFailHandler when a validator is set and validation fails', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event = sinon.createStubInstance(EventInterface); + + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult = new ValidationResult(validator, event, false, ['should have required property \'user\'']); + + validator.validate = sinon.stub() + .returns(Promise.resolve(validationResult)); + + let validationFailHandler = sinon.spy(); + + let manager = new EventManager(adapter, translator, validator); + manager.validationFailHandler = validationFailHandler; + + return manager.dispatch('my_channel', event).catch((reason) => { + sinon.assert.calledOnce(validator.validate); + sinon.assert.calledWith(validator.validate, event); + + sinon.assert.calledOnce(validationFailHandler); + sinon.assert.calledWith(validationFailHandler, validationResult); + + expect(reason).to.equal(validationResult); + }); }); it('should automagically inject values from attribute injectors into the message payload but not override conflicting attributes', () => { let adapter = sinon.createStubInstance(PubSubAdapterInterface); - adapter.publish = sinon.stub(); + adapter.publish = sinon.stub() + .returns(Promise.resolve('result')); let translator = sinon.createStubInstance(MessageTranslatorInterface); let manager = new EventManager(adapter, translator); @@ -503,25 +604,28 @@ describe('EventManager', () => { let event = new SimpleEvent('user.created', {'service': 'www'}); - manager.dispatch('my_channel', event); - - sinon.assert.calledOnce(adapter.publish); - sinon.assert.calledWith( - adapter.publish, - 'my_channel', - { - 'event': 'user.created', - 'service': 'www', - 'hello': 'world', - } - ); + return manager.dispatch('my_channel', event).then((result) => { + sinon.assert.calledOnce(adapter.publish); + sinon.assert.calledWith( + adapter.publish, + 'my_channel', + { + 'event': 'user.created', + 'service': 'www', + 'hello': 'world', + } + ); + + expect(result).to.equal('result'); + }); }); }); describe('dispatchBatch', () => { - it('should convert the events to an array of message and publish to the channel', () => { + it('should convert the events to an array of message and publish to the channel', () => { let adapter = sinon.createStubInstance(PubSubAdapterInterface); - adapter.publishBatch = sinon.stub(); + adapter.publishBatch = sinon.stub() + .returns(Promise.resolve(['result1', 'result2'])); let translator = sinon.createStubInstance(MessageTranslatorInterface); let manager = new EventManager(adapter, translator); @@ -536,25 +640,28 @@ describe('EventManager', () => { let events = [event1, event2]; - manager.dispatchBatch('my_channel', events); - - sinon.assert.calledOnce(event1.toMessage); - sinon.assert.calledOnce(event2.toMessage); - - sinon.assert.calledOnce(adapter.publishBatch); - sinon.assert.calledWith( - adapter.publishBatch, - 'my_channel', - [ - {'event': 'user.created'}, - {'event': 'order.created'}, - ] - ); + return manager.dispatchBatch('my_channel', events).then((results) => { + sinon.assert.calledOnce(event1.toMessage); + sinon.assert.calledOnce(event2.toMessage); + + sinon.assert.calledOnce(adapter.publishBatch); + sinon.assert.calledWith( + adapter.publishBatch, + 'my_channel', + [ + {'event': 'user.created'}, + {'event': 'order.created'}, + ] + ); + + expect(results).to.deep.equal(['result1', 'result2']); + }); }); it('should automagically inject values from attribute injectors into the message payload', () => { let adapter = sinon.createStubInstance(PubSubAdapterInterface); - adapter.publishBatch = sinon.stub(); + adapter.publishBatch = sinon.stub() + .returns(Promise.resolve(['result1', 'result2'])); let translator = sinon.createStubInstance(MessageTranslatorInterface); let manager = new EventManager(adapter, translator); @@ -567,25 +674,142 @@ describe('EventManager', () => { let events = [event1, event2]; - manager.dispatchBatch('my_channel', events); + return manager.dispatchBatch('my_channel', events).then((results) => { + sinon.assert.calledOnce(adapter.publishBatch); + sinon.assert.calledWith( + adapter.publishBatch, + 'my_channel', + [ + { + 'event': 'user.created', + 'service': 'search', + 'hello': 'world', + }, + { + 'event': 'order.created', + 'service': 'search', + 'hello': 'world', + }, + ] + ); + + expect(results).to.deep.equal(['result1', 'result2']); + }); + }); - sinon.assert.calledOnce(adapter.publishBatch); - sinon.assert.calledWith( - adapter.publishBatch, - 'my_channel', - [ - { - 'event': 'user.created', - 'service': 'search', - 'hello': 'world', - }, - { - 'event': 'order.created', - 'service': 'search', - 'hello': 'world', - }, - ] - ); + it('should convert the events to an array of message and publish to the channel, when a validator is set and validation passes', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + adapter.publishBatch = sinon.stub() + .returns(Promise.resolve(['result1', 'result2'])); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event1 = sinon.createStubInstance(EventInterface); + event1.toMessage = sinon.stub() + .returns({'event': 'user.created'}); + + let event2 = sinon.createStubInstance(EventInterface); + event2.toMessage = sinon.stub() + .returns({'event': 'order.created'}); + let events = [event1, event2]; + + let validator = sinon.createStubInstance(EventValidatorInterface); + validator.validate = sinon.stub(); + validator.validate.onCall(0) + .returns(Promise.resolve(new ValidationResult(validator, event1, true))); + validator.validate.onCall(1) + .returns(Promise.resolve(new ValidationResult(validator, event2, true))); + + let manager = new EventManager(adapter, translator, validator); + + return manager.dispatchBatch('my_channel', events).then((results) => { + sinon.assert.calledOnce(event1.toMessage); + sinon.assert.calledOnce(event2.toMessage); + + sinon.assert.calledTwice(validator.validate); + sinon.assert.calledWith(validator.validate, event1); + sinon.assert.calledWith(validator.validate, event2); + + sinon.assert.calledOnce(adapter.publishBatch); + sinon.assert.calledWith( + adapter.publishBatch, + 'my_channel', + [ + {'event': 'user.created'}, + {'event': 'order.created'}, + ] + ); + + expect(results).to.deep.equal(['result1', 'result2']); + }); + }); + + it('should reject the promise with the first failed ValidationResult when a validator is set and validation fails', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event1 = sinon.createStubInstance(EventInterface); + let event2 = sinon.createStubInstance(EventInterface); + let events = [event1, event2]; + + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult1 = new ValidationResult(validator, event1, false, ['should have required property \'user\'']); + let validationResult2 = new ValidationResult(validator, event2, true); + + validator.validate = sinon.stub(); + validator.validate.onCall(0) + .returns(Promise.resolve(validationResult1)); + validator.validate.onCall(1) + .returns(Promise.resolve(validationResult2)); + + let manager = new EventManager(adapter, translator, validator); + + return manager.dispatchBatch('my_channel', events).catch((reason) => { + sinon.assert.calledTwice(validator.validate); + sinon.assert.calledWith(validator.validate, event1); + sinon.assert.calledWith(validator.validate, event2); + + expect(reason).to.equal(validationResult1); + }); + }); + + it('should call the validationFailHandler when a validator is set and validation fails', () => { + let adapter = sinon.createStubInstance(PubSubAdapterInterface); + + let translator = sinon.createStubInstance(MessageTranslatorInterface); + + let event1 = sinon.createStubInstance(EventInterface); + let event2 = sinon.createStubInstance(EventInterface); + let events = [event1, event2]; + + let validator = sinon.createStubInstance(EventValidatorInterface); + + let validationResult1 = new ValidationResult(validator, event1, false, ['should have required property \'user\'']); + let validationResult2 = new ValidationResult(validator, event2, true); + + validator.validate = sinon.stub(); + validator.validate.onCall(0) + .returns(Promise.resolve(validationResult1)); + validator.validate.onCall(1) + .returns(Promise.resolve(validationResult2)); + + let validationFailHandler = sinon.spy(); + + let manager = new EventManager(adapter, translator, validator); + manager.validationFailHandler = validationFailHandler; + + return manager.dispatchBatch('my_channel', events).catch((reason) => { + sinon.assert.calledTwice(validator.validate); + sinon.assert.calledWith(validator.validate, event1); + sinon.assert.calledWith(validator.validate, event2); + + sinon.assert.calledOnce(validationFailHandler); + sinon.assert.calledWith(validationFailHandler, validationResult1); + + expect(reason).to.equal(validationResult1); + }); }); }); }); diff --git a/test/ValidationResultTest.js b/test/ValidationResultTest.js new file mode 100644 index 0000000..5a717c5 --- /dev/null +++ b/test/ValidationResultTest.js @@ -0,0 +1,47 @@ +'use strict'; + +let chai = require('chai'); +let expect = chai.expect; +let sinon = require('sinon'); +let EventValidatorInterface = require('../lib/EventValidatorInterface'); +let EventInterface = require('../lib/EventInterface'); +let ValidationResult = require('../lib/ValidationResult'); + +describe('ValidationResult', () => { + describe('construct instance', () => { + it('should set the validator property', () => { + let validator = sinon.createStubInstance(EventValidatorInterface); + let event = sinon.createStubInstance(EventInterface); + let result = new ValidationResult(validator, event, true, []); + expect(result.validator).to.equal(validator); + }); + + it('should set the event property', () => { + let validator = sinon.createStubInstance(EventValidatorInterface); + let event = sinon.createStubInstance(EventInterface); + let result = new ValidationResult(validator, event, true, []); + expect(result.event).to.equal(event); + }); + + it('should set the passes property', () => { + let validator = sinon.createStubInstance(EventValidatorInterface); + let event = sinon.createStubInstance(EventInterface); + let result = new ValidationResult(validator, event, true, []); + expect(result.passes).to.be.true; + }); + + it('should set the fails property', () => { + let validator = sinon.createStubInstance(EventValidatorInterface); + let event = sinon.createStubInstance(EventInterface); + let result = new ValidationResult(validator, event, true, []); + expect(result.fails).to.be.false; + }); + + it('should set the errors property', () => { + let validator = sinon.createStubInstance(EventValidatorInterface); + let event = sinon.createStubInstance(EventInterface); + let result = new ValidationResult(validator, event, true, ['should have required property \'user\'']); + expect(result.errors).to.deep.equal(['should have required property \'user\'']); + }); + }); +}); diff --git a/test/ValidatorsTest.js b/test/ValidatorsTest.js index c6cc347..bfc3731 100644 --- a/test/ValidatorsTest.js +++ b/test/ValidatorsTest.js @@ -2,12 +2,12 @@ let chai = require('chai'); let expect = chai.expect; -let should = chai.should; let chaiAsPromised = require('chai-as-promised'); let sinon = require('sinon'); let Ajv = require('ajv'); let JSONSchemaEventValidator = require('../lib/validators/JSONSchemaEventValidator'); let SchemaEvent = require('../lib/events/SchemaEvent'); +let ValidationResult = require('../lib/ValidationResult'); chai.use(chaiAsPromised); chai.should(); @@ -27,7 +27,7 @@ describe('Validators', () => { }); }); - describe('validates', () => { + describe('validate', () => { it('should return a promise', () => { let ajv = sinon.createStubInstance(Ajv); ajv.compileAsync = sinon.stub() @@ -37,12 +37,12 @@ describe('Validators', () => { let event = new SchemaEvent('http://schemas.my-website.org/events/user/created/1.0.json'); - let promise = validator.validates(event); + let promise = validator.validate(event); expect(promise).to.be.a('promise'); }); - it('should resolve a promise to true if validation passes', () => { + it('should resolve a promise to a ValidationResult if validation passes', () => { let validate = sinon.stub() .returns(true); @@ -56,19 +56,26 @@ describe('Validators', () => { let event = new SchemaEvent('http://schemas.my-website.org/events/user/created/1.0.json'); - return validator.validates(event) - .then((success) => { - sinon.assert.calledOnce(validate); - sinon.assert.calledWith(validate, {'schema': 'http://schemas.my-website.org/events/user/created/1.0.json'}); + return validator.validate(event).then((result) => { + sinon.assert.calledOnce(validate); + sinon.assert.calledWith(validate, {'schema': 'http://schemas.my-website.org/events/user/created/1.0.json'}); - return success; - }) - .should.eventually.be.true; + expect(result).to.be.an.instanceof(ValidationResult); + expect(result.validator).to.equal(validator); + expect(result.event).to.equal(event); + expect(result.passes).to.be.true; + expect(result.errors).to.be.empty; + }); }); it('should resolve a promise to false if validation fails', () => { let validate = sinon.stub() .returns(false); + validate.errors = [ + { + message: 'should have required property \'user\'', + }, + ]; let promise = new Promise((resolve, reject) => resolve(validate)); @@ -80,14 +87,16 @@ describe('Validators', () => { let event = new SchemaEvent('http://schemas.my-website.org/events/user/created/1.0.json'); - return validator.validates(event) - .catch((errors) => { - sinon.assert.calledOnce(validate); - sinon.assert.calledWith(validate, {'schema': 'http://schemas.my-website.org/events/user/created/1.0.json'}); + return validator.validate(event).then((result) => { + sinon.assert.calledOnce(validate); + sinon.assert.calledWith(validate, {'schema': 'http://schemas.my-website.org/events/user/created/1.0.json'}); - throw errors; - }) - .should.eventually.be.false; + expect(result).to.be.an.instanceof(ValidationResult); + expect(result.validator).to.equal(validator); + expect(result.event).to.equal(event); + expect(result.passes).to.be.false; + expect(result.errors).to.deep.equal(['should have required property \'user\'']); + }); }); });