From 8726142664cd182089864bb628be1877f937952a Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 26 Jun 2019 15:48:42 +0200 Subject: [PATCH 1/6] feat: adds "kind", "values" field properties, removes deprecations BREAKING CHANGE: - Gavel fields no longer contain the following properties: - `validator` - `expectedType` - `realType` - `rawData` - Gavel fields now contain the next new properies: - `kind` - `values` (optional) --- lib/units/validateBody.js | 42 +++-- lib/units/validateHeaders.js | 11 +- lib/units/validateMethod.js | 18 +- lib/units/validateStatusCode.js | 21 +-- lib/units/validateURI.js | 26 +-- lib/validate.js | 2 +- test/chai.js | 157 ++++++------------ test/integration/validate.test.js | 129 +++----------- test/unit/units/validateBody.test.js | 141 ++++------------ .../validateBody/getBodyValidator.test.js | 1 + test/unit/units/validateHeaders.test.js | 44 +---- test/unit/units/validateMethod.test.js | 54 +++--- test/unit/units/validateStatusCode.test.js | 33 ++-- test/unit/units/validateURI.test.js | 132 ++++++--------- 14 files changed, 268 insertions(+), 543 deletions(-) diff --git a/lib/units/validateBody.js b/lib/units/validateBody.js index a4d25ad5..b7ff5ad3 100644 --- a/lib/units/validateBody.js +++ b/lib/units/validateBody.js @@ -122,16 +122,17 @@ function getBodyValidator(realType, expectedType) { }; const validators = [ - [TextDiff, both(isPlainText)], + [TextDiff, both(isPlainText), 'text'], // List JsonSchema first, because weak predicate of JsonExample // would resolve on "application/schema+json" media type too. [ JsonSchema, (real, expected) => { return isJson(real) && isJsonSchema(expected); - } + }, + 'json' ], - [JsonExample, both(isJson)] + [JsonExample, both(isJson), 'json'] ]; const validator = validators.find(([_name, predicate]) => { @@ -142,10 +143,10 @@ function getBodyValidator(realType, expectedType) { const error = `Can't validate real media type '${mediaTyper.format( realType )}' against expected media type '${mediaTyper.format(expectedType)}'.`; - return [error, null]; + return [error, null, null]; } - return [null, validator[0]]; + return [null, validator[0], validator[2]]; } /** @@ -157,6 +158,10 @@ function validateBody(expected, real) { const errors = []; const realBodyType = typeof real.body; const hasEmptyRealBody = real.body === ''; + const values = { + expected: expected.body, + actual: real.body + }; // Throw when user input for real body is not a string. if (realBodyType !== 'string') { @@ -180,13 +185,15 @@ function validateBody(expected, real) { if (realTypeError) { errors.push({ - message: realTypeError + message: realTypeError, + values }); } if (expectedTypeError) { errors.push({ - message: expectedTypeError + message: expectedTypeError, + values }); } @@ -194,8 +201,8 @@ function validateBody(expected, real) { // Skipping body validation in case errors during // real/expected body type definition. - const [validatorError, ValidatorClass] = hasErrors - ? [null, null] + const [validatorError, ValidatorClass, kind] = hasErrors + ? [null, null, null] : getBodyValidator(realType, expectedType); if (validatorError) { @@ -208,11 +215,13 @@ function validateBody(expected, real) { errors.push({ message: `Expected "body" of "${mediaTyper.format( expectedType - )}" media type, but actual "body" is missing.` + )}" media type, but actual "body" is missing.`, + values }); } else { errors.push({ - message: validatorError + message: validatorError, + values }); } } @@ -224,16 +233,15 @@ function validateBody(expected, real) { real.body, usesJsonSchema ? expected.bodySchema : expected.body ); - const rawData = validator && validator.validate(); + // Without ".validate()" it cannot evaluate output to result. + // TODO Re-do this. + validator && validator.validate(); const validationErrors = validator ? validator.evaluateOutputToResults() : []; errors.push(...validationErrors); return { - isValid: isValidField({ errors }), - validator: ValidatorClass && ValidatorClass.name, - realType: mediaTyper.format(realType), - expectedType: mediaTyper.format(expectedType), - rawData, + valid: isValidField({ errors }), + kind, errors }; } diff --git a/lib/units/validateHeaders.js b/lib/units/validateHeaders.js index bcfed620..4ca8ebc8 100644 --- a/lib/units/validateHeaders.js +++ b/lib/units/validateHeaders.js @@ -26,7 +26,9 @@ function validateHeaders(expected, real) { const validator = hasJsonHeaders ? new HeadersJsonExample(real.headers, expected.headers) : null; - const rawData = validator && validator.validate(); + + // if you don't call ".validate()", it never evaluates any results. + validator && validator.validate(); if (validator) { errors.push(...validator.evaluateOutputToResults()); @@ -42,11 +44,8 @@ and expected data media type } return { - isValid: isValidField({ errors }), - validator: validator && 'HeadersJsonExample', - realType, - expectedType, - rawData, + valid: isValidField({ errors }), + kind: hasJsonHeaders ? 'json' : 'text', errors }; } diff --git a/lib/units/validateMethod.js b/lib/units/validateMethod.js index ef14f144..1390b792 100644 --- a/lib/units/validateMethod.js +++ b/lib/units/validateMethod.js @@ -1,22 +1,22 @@ -const APIARY_METHOD_TYPE = 'text/vnd.apiary.method'; - function validateMethod(expected, real) { const { method: expectedMethod } = expected; const { method: realMethod } = real; - const isValid = realMethod === expectedMethod; + const valid = realMethod === expectedMethod; const errors = []; - if (!isValid) { + if (!valid) { errors.push({ - message: `Expected "method" field to equal "${expectedMethod}", but got "${realMethod}".` + message: `Expected "method" field to equal "${expectedMethod}", but got "${realMethod}".`, + values: { + expected: expectedMethod, + actual: realMethod + } }); } return { - isValid, - validator: null, - realType: APIARY_METHOD_TYPE, - expectedType: APIARY_METHOD_TYPE, + valid, + kind: 'text', errors }; } diff --git a/lib/units/validateStatusCode.js b/lib/units/validateStatusCode.js index 5c757514..36d5ac2a 100644 --- a/lib/units/validateStatusCode.js +++ b/lib/units/validateStatusCode.js @@ -1,28 +1,25 @@ -const APIARY_STATUS_CODE_TYPE = 'text/vnd.apiary.status-code'; - /** * Validates given real and expected status codes. * @param {Object} real * @param {number} expected */ function validateStatusCode(expected, real) { - const isValid = real.statusCode === expected.statusCode; + const valid = real.statusCode === expected.statusCode; const errors = []; - if (!isValid) { + if (!valid) { errors.push({ - message: `Status code is '${real.statusCode}' instead of '${ - expected.statusCode - }'` + message: `Status code is '${real.statusCode}' instead of '${expected.statusCode}'`, + values: { + expected: expected.statusCode, + actual: real.statusCode + } }); } return { - isValid, - validator: 'TextDiff', - realType: APIARY_STATUS_CODE_TYPE, - expectedType: APIARY_STATUS_CODE_TYPE, - rawData: '', + valid, + kind: 'text', errors }; } diff --git a/lib/units/validateURI.js b/lib/units/validateURI.js index 7f769c11..cdb4eb6b 100644 --- a/lib/units/validateURI.js +++ b/lib/units/validateURI.js @@ -1,8 +1,6 @@ const url = require('url'); const deepEqual = require('deep-equal'); -const APIARY_URI_TYPE = 'text/vnd.apiary.uri'; - /** * Parses the given URI and returns the properties * elligible for comparison. Leaves out raw properties like "path" @@ -20,32 +18,34 @@ const parseURI = (uri) => { }; }; -const validateURI = (expected, real) => { +const validateURI = (expected, actual) => { const { uri: expectedURI } = expected; - const { uri: realURI } = real; + const { uri: actualURI } = actual; // Parses URI to perform a correct comparison: // - literal comparison of pathname // - order-insensitive comparison of query parameters - const parsedExpected = parseURI(expectedURI, true); - const parsedReal = parseURI(realURI, true); + const parsedExpected = parseURI(expectedURI); + const parsedActual = parseURI(actualURI); // Note the different order of arguments between // "validateURI" and "deepEqual". - const isValid = deepEqual(parsedReal, parsedExpected); + const valid = deepEqual(parsedActual, parsedExpected); const errors = []; - if (!isValid) { + if (!valid) { errors.push({ - message: `Expected "uri" field to equal "${expectedURI}", but got: "${realURI}".` + message: `Expected "uri" field to equal "${expectedURI}", but got: "${actualURI}".`, + values: { + expected: expectedURI, + actual: actualURI + } }); } return { - isValid, - validator: null, - expectedType: APIARY_URI_TYPE, - realType: APIARY_URI_TYPE, + valid, + kind: 'text', errors }; }; diff --git a/lib/validate.js b/lib/validate.js index ba456b72..74e9a15e 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -47,7 +47,7 @@ function validate(expectedMessage, realMessage) { } // Indicates the validity of the real message - result.isValid = isValidResult(result); + result.valid = isValidResult(result); return result; } diff --git a/test/chai.js b/test/chai.js index 43b6ea6f..2e18dafa 100644 --- a/test/chai.js +++ b/test/chai.js @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ const chai = require('chai'); +const deepEqual = require('deep-equal'); const stringify = (obj) => { return JSON.stringify(obj, null, 2); @@ -18,31 +19,33 @@ chai.use(({ Assertion }, utils) => { new Assertion(error).to.have.property(propName); this.assert( - isRegExp ? expectedValue.test(target) : target === expectedValue, + isRegExp + ? expectedValue.test(target) + : deepEqual(target, expectedValue), ` - Expected the next HTTP message field: - - ${stringifiedObj} - - to have ${propName} at index ${currentErrorIndex} that ${matchWord}: - - ${expectedValue.toString()} - - but got: - - ${target.toString()} +Expected the next HTTP message field: + +${stringifiedObj} + +to have an error at index ${currentErrorIndex} that includes property "${propName}" that ${matchWord}: + +${JSON.stringify(expectedValue)} + +but got: + +${JSON.stringify(target)} `, ` - Expected the next HTTP message field: - - ${stringifiedObj} - - not to have ${propName} at index ${currentErrorIndex}, but got: - - ${target.toString()} +Expected the next HTTP message field: + +${stringifiedObj} + +to have an error at index ${currentErrorIndex} that includes property "${propName}" that not ${matchWord}: + +${JSON.stringify(target)} `, - expectedValue.toString(), - target.toString(), + JSON.stringify(expectedValue), + JSON.stringify(target), true ); }); @@ -50,19 +53,43 @@ chai.use(({ Assertion }, utils) => { createErrorPropertyAssertion('message', 'withMessage'); createErrorPropertyAssertion('pointer', 'withPointer'); + createErrorPropertyAssertion('values', 'withValues'); + + // + // TODO + // Finish the error messages + Assertion.addMethod('kind', function(expectedValue) { + const { kind } = this._obj; + const stringifiedObj = stringify(this._obj); + + this.assert( + kind === expectedValue, + ` +Expected the following HTTP message field: + +${stringifiedObj} + +to have "kind" property equal to "${expectedValue}". + `, + 'asdas', + expectedValue, + kind, + true + ); + }); utils.addProperty(Assertion.prototype, 'valid', function() { - const { isValid } = this._obj; + const { valid } = this._obj; const stringifiedObj = stringify(this._obj); this.assert( - isValid === true, + valid === true, ` Expected the following HTTP message field: ${stringifiedObj} -to have "isValid" equal #{exp}, but got #{act}'. +to have "valid" equal #{exp}, but got #{act}'. `, ` Expected the following HTTP message field: @@ -70,8 +97,8 @@ Expected the following HTTP message field: ${stringifiedObj} to be invalid, but it is actually valid.`, - { isValid }, - { isValid: true }, + { valid }, + { valid: true }, true ); }); @@ -120,84 +147,6 @@ to have no errors, but got ${errors.length} error(s). utils.flag(this, 'currentError', errors[index]); utils.flag(this, 'currentErrorIndex', index); }); - - Assertion.addMethod('validator', function(expectedValue) { - const { validator: actualValue } = this._obj; - const stringifiedObj = stringify(this._obj); - - this.assert( - actualValue === expectedValue, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to have "${expectedValue}" validator, but got "${actualValue}". - `, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to not have validator equal to "${expectedValue}". -`, - expectedValue, - actualValue, - true - ); - }); - - Assertion.addMethod('expectedType', function(expectedValue) { - const { expectedType: actualValue } = this._obj; - const stringifiedObj = stringify(this._obj); - - this.assert( - actualValue === expectedValue, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to have an "expectedType" equal to "${expectedValue}", but got "${actualValue}". - `, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to not have an "expectedType" of "${expectedValue}". - `, - expectedValue, - actualValue, - true - ); - }); - - Assertion.addMethod('realType', function(expectedValue) { - const { realType: actualValue } = this._obj; - const stringifiedObj = stringify(this._obj); - - this.assert( - actualValue === expectedValue, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to have an "realType" equal to "${expectedValue}", but got "${actualValue}". -`, - ` -Expected the following HTTP message field: - -${stringifiedObj} - -to not have an "realType" of "${expectedValue}". - `, - expectedValue, - actualValue, - true - ); - }); }); module.exports = chai; diff --git a/test/integration/validate.test.js b/test/integration/validate.test.js index a6e2baaf..639407ef 100644 --- a/test/integration/validate.test.js +++ b/test/integration/validate.test.js @@ -22,31 +22,19 @@ describe('validate', () => { describe('method', () => { expect(result.fields.method).to.be.valid; - expect(result.fields.method).to.have.validator(null); - expect(result.fields.method).to.have.expectedType( - 'text/vnd.apiary.method' - ); - expect(result.fields.method).to.have.realType('text/vnd.apiary.method'); + expect(result.fields.method).to.have.kind('text'); expect(result.fields.method).to.not.have.errors; }); describe('headers', () => { expect(result.fields.headers).to.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); + expect(result.fields.headers).to.have.kind('json'); expect(result.fields.headers).to.not.have.errors; }); describe('body', () => { expect(result.fields.body).to.be.valid; - expect(result.fields.body).to.have.validator('JsonExample'); - expect(result.fields.body).to.have.expectedType('application/json'); - expect(result.fields.body).to.have.realType('application/json'); + expect(result.fields.body).to.have.kind('json'); expect(result.fields.body).to.not.have.errors; }); }); @@ -77,12 +65,7 @@ describe('validate', () => { describe('method', () => { expect(result.fields.method).to.not.be.valid; - expect(result.fields.method).to.have.validator(null); - expect(result.fields.method).to.have.expectedType( - 'text/vnd.apiary.method' - ); - expect(result.fields.method).to.have.realType('text/vnd.apiary.method'); - + expect(result.fields.method).to.have.kind('text'); describe('produces one error', () => { it('exactly one error', () => { expect(result.fields.method).to.have.errors.lengthOf(1); @@ -100,21 +83,13 @@ describe('validate', () => { describe('headers', () => { expect(result.fields.headers).to.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); + expect(result.fields.headers).to.have.kind('json'); expect(result.fields.headers).to.not.have.errors; }); describe('body', () => { expect(result.fields.body).to.not.be.valid; - expect(result.fields.body).to.have.validator('JsonExample'); - expect(result.fields.body).to.have.expectedType('application/json'); - expect(result.fields.body).to.have.realType('application/json'); + expect(result.fields.body).to.have.kind('json'); describe('produces an error', () => { it('exactly one error', () => { @@ -150,33 +125,19 @@ describe('validate', () => { describe('statusCode', () => { expect(result.fields.statusCode).to.be.valid; - expect(result.fields.statusCode).to.have.validator('TextDiff'); - expect(result.fields.statusCode).to.have.expectedType( - 'text/vnd.apiary.status-code' - ); - expect(result.fields.statusCode).to.have.realType( - 'text/vnd.apiary.status-code' - ); + expect(result.fields.statusCode).to.have.kind('text'); expect(result.fields.statusCode).to.not.have.errors; }); describe('headers', () => { expect(result.fields.headers).to.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); + expect(result.fields.headers).to.have.kind('json'); expect(result.fields.headers).to.not.have.errors; }); describe('body', () => { expect(result.fields.body).to.be.valid; - expect(result.fields.body).to.have.validator('JsonExample'); - expect(result.fields.body).to.have.expectedType('application/json'); - expect(result.fields.body).to.have.realType('application/json'); + expect(result.fields.body).to.have.kind('json'); expect(result.fields.body).to.not.have.errors; }); }); @@ -206,14 +167,7 @@ describe('validate', () => { describe('statusCode', () => { expect(result.fields.statusCode).to.not.be.valid; - expect(result.fields.statusCode).to.have.validator('TextDiff'); - expect(result.fields.statusCode).to.have.expectedType( - 'text/vnd.apiary.status-code' - ); - expect(result.fields.statusCode).to.have.realType( - 'text/vnd.apiary.status-code' - ); - + expect(result.fields.statusCode).to.have.kind('text'); describe('produces an error', () => { it('exactly one error', () => { expect(result.fields.statusCode).to.have.errors.lengthOf(1); @@ -229,13 +183,7 @@ describe('validate', () => { describe('headers', () => { expect(result.fields.headers).to.not.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); + expect(result.fields.headers).to.have.kind('json'); describe('produces an error', () => { it('exactly one error', () => { @@ -276,26 +224,13 @@ describe('validate', () => { describe('statusCode', () => { expect(result.fields.statusCode).to.be.valid; - expect(result.fields.statusCode).to.have.validator('TextDiff'); - expect(result.fields.statusCode).to.have.expectedType( - 'text/vnd.apiary.status-code' - ); - expect(result.fields.statusCode).to.have.realType( - 'text/vnd.apiary.status-code' - ); + expect(result.fields.statusCode).to.have.kind('text'); expect(result.fields.statusCode).to.not.have.errors; }); describe('headers', () => { expect(result.fields.headers).to.not.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); - + expect(result.fields.headers).to.have.kind('json'); describe('produces an error', () => { it('exactly one error', () => { expect(result.fields.headers).to.have.errors.lengthOf(1); @@ -349,11 +284,7 @@ describe('validate', () => { describe('for properties present in both expected and real', () => { describe('method', () => { expect(result.fields.method).to.not.be.valid; - expect(result.fields.method).to.have.validator(null); - expect(result.fields.method).to.have.expectedType( - 'text/vnd.apiary.method' - ); - expect(result.fields.method).to.have.realType('text/vnd.apiary.method'); + expect(result.fields.method).to.have.kind('text'); describe('produces an error', () => { it('exactly one error', () => { @@ -367,6 +298,15 @@ describe('validate', () => { 'Expected "method" field to equal "POST", but got "PUT".' ); }); + + it('includes values', () => { + expect(result.fields.method) + .to.have.errorAtIndex(0) + .withValues({ + expected: 'POST', + actual: 'PUT' + }); + }); }); }); }); @@ -374,14 +314,7 @@ describe('validate', () => { describe('for properties present in expected, but not in real', () => { describe('statusCode', () => { expect(result.fields.statusCode).to.not.be.valid; - expect(result.fields.statusCode).to.have.validator('TextDiff'); - expect(result.fields.statusCode).to.have.expectedType( - 'text/vnd.apiary.status-code' - ); - expect(result.fields.statusCode).to.have.realType( - 'text/vnd.apiary.status-code' - ); - + expect(result.fields.statusCode).to.have.kind('text'); describe('produces an error', () => { it('exactly one error', () => { expect(result.fields.statusCode).to.have.errors.lengthOf(1); @@ -397,14 +330,7 @@ describe('validate', () => { describe('headers', () => { expect(result.fields.headers).to.not.be.valid; - expect(result.fields.headers).to.have.validator('HeadersJsonExample'); - expect(result.fields.headers).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); - expect(result.fields.headers).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); - + expect(result.fields.headers).to.have.kind('json'); describe('produces one error', () => { it('exactly one error', () => { expect(result.fields.headers).to.have.errors.lengthOf(1); @@ -422,10 +348,7 @@ describe('validate', () => { describe('body', () => { expect(result.fields.body).to.not.be.valid; - expect(result.fields.body).to.have.validator(null); - expect(result.fields.body).to.have.expectedType('application/json'); - expect(result.fields.body).to.have.realType('text/plain'); - + expect(result.fields.body).to.have.kind(null); describe('produces an error', () => { it('exactly one error', () => { expect(result.fields.body).to.have.errors.lengthOf(1); diff --git a/test/unit/units/validateBody.test.js b/test/unit/units/validateBody.test.js index 7fa92db1..7373c426 100644 --- a/test/unit/units/validateBody.test.js +++ b/test/unit/units/validateBody.test.js @@ -35,16 +35,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has no validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "text/plain" expected type', () => { - expect(result).to.have.expectedType('text/plain'); + it('has "null" kind', () => { + expect(result).to.have.kind(null); }); describe('produces validation error', () => { @@ -59,6 +51,15 @@ describe('validateBody', () => { `Can't validate real media type 'application/json' against expected media type 'text/plain'.` ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '', + actual: '{ "foo": "bar" }' + }); + }); }); }); @@ -79,16 +80,8 @@ describe('validateBody', () => { expect(result).to.be.valid; }); - it('has "JsonExample" validator', () => { - expect(result).to.have.validator('JsonExample'); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "application/json" expected type', () => { - expect(result).to.have.expectedType('application/json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); it('has no errors', () => { @@ -111,16 +104,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has no validator', () => { - expect(result).to.have.validator(null); - }); - - it('fallbacks to "text/plain" real type', () => { - expect(result).to.have.realType('text/plain'); - }); - - it('has "application/json" expected type', () => { - expect(result).to.have.expectedType('application/json'); + it('has "null" kind', () => { + expect(result).to.have.kind(null); }); describe('produces content-type error', () => { @@ -157,16 +142,8 @@ describe('validateBody', () => { expect(result).to.be.valid; }); - it('has "JsonExample" validator', () => { - expect(result).to.have.validator('JsonExample'); - }); - - it('has "application/hal+json" real type', () => { - expect(result).to.have.realType('application/hal+json'); - }); - - it('has "application/json" expected type', () => { - expect(result).to.have.expectedType('application/json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); it('has no errors', () => { @@ -191,16 +168,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has no validator', () => { - expect(result).to.have.validator(null); - }); - - it('fallbacks to "text/plain" real type', () => { - expect(result).to.have.realType('text/plain'); - }); - - it('has "text/plain" expected type', () => { - expect(result).to.have.expectedType('text/plain'); + it('has "null" kind', () => { + expect(result).to.have.kind(null); }); describe('produces error', () => { @@ -236,16 +205,8 @@ describe('validateBody', () => { expect(result).to.be.valid; }); - it('has "TextDiff" validator', () => { - expect(result).to.have.validator('TextDiff'); - }); - - it('has text/plain real type', () => { - expect(result).to.have.realType('text/plain'); - }); - - it('has "text/plain" expected type', () => { - expect(result).to.have.expectedType('text/plain'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -267,16 +228,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has "TextDiff" validator', () => { - expect(result).to.have.validator('TextDiff'); - }); - - it('has "text/plain" real type', () => { - expect(result).to.have.realType('text/plain'); - }); - - it('has "text/plain" expected type', () => { - expect(result).to.have.expectedType('text/plain'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces validation error', () => { @@ -308,16 +261,8 @@ describe('validateBody', () => { expect(result).to.be.valid; }); - it('has "JsonExample" validator', () => { - expect(result).to.have.validator('JsonExample'); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "application/json" expected type', () => { - expect(result).to.have.expectedType('application/json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); it('has no errors', () => { @@ -339,16 +284,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has "JsonExample" validator', () => { - expect(result).to.have.validator('JsonExample'); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "application/json" expected type', () => { - expect(result).to.have.expectedType('application/json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); describe('produces validation errors', () => { @@ -382,16 +319,8 @@ describe('validateBody', () => { expect(result).to.be.valid; }); - it('has "JsonSchema" validator', () => { - expect(result).to.have.validator('JsonSchema'); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "application/schema+json" expected type', () => { - expect(result).to.have.expectedType('application/schema+json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); it('has no errors', () => { @@ -415,16 +344,8 @@ describe('validateBody', () => { expect(result).to.not.be.valid; }); - it('has "JsonSchema" validator', () => { - expect(result).to.have.validator('JsonSchema'); - }); - - it('has "application/json" real type', () => { - expect(result).to.have.realType('application/json'); - }); - - it('has "application/schema+json" expected type', () => { - expect(result).to.have.expectedType('application/schema+json'); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); describe('produces an error', () => { diff --git a/test/unit/units/validateBody/getBodyValidator.test.js b/test/unit/units/validateBody/getBodyValidator.test.js index 8406eb90..a83e7579 100644 --- a/test/unit/units/validateBody/getBodyValidator.test.js +++ b/test/unit/units/validateBody/getBodyValidator.test.js @@ -44,6 +44,7 @@ describe('getBodyValidator', () => { }); }); + // TODO Remove or uncomment // describe('when given unknown media type', () => { // const unknownContentTypes = [['text/html', 'text/xml']]; diff --git a/test/unit/units/validateHeaders.test.js b/test/unit/units/validateHeaders.test.js index 6a4d4c75..6d45767e 100644 --- a/test/unit/units/validateHeaders.test.js +++ b/test/unit/units/validateHeaders.test.js @@ -22,20 +22,8 @@ describe('validateHeaders', () => { expect(result).to.be.valid; }); - it('has "HeadersJsonExample" validator', () => { - expect(result).to.have.validator('HeadersJsonExample'); - }); - - it('has "application/vnd.apiary.http-headers+json" real type', () => { - expect(result).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); - }); - - it('has "application/vnd.apiary.http-headers+json" expected type', () => { - expect(result).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); it('has no errors', () => { @@ -63,20 +51,8 @@ describe('validateHeaders', () => { expect(result).to.not.be.valid; }); - it('has "HeadersJsonExample" validator', () => { - expect(result).to.have.validator('HeadersJsonExample'); - }); - - it('has "application/vnd.apiary.http-headers+json" real type', () => { - expect(result).to.have.realType( - 'application/vnd.apiary.http-headers+json' - ); - }); - - it('has "application/vnd.apiary.http-headers+json" expected type', () => { - expect(result).to.have.expectedType( - 'application/vnd.apiary.http-headers+json' - ); + it('has "json" kind', () => { + expect(result).to.have.kind('json'); }); describe('produces errors', () => { @@ -122,16 +98,8 @@ describe('validateHeaders', () => { expect(result).to.not.be.valid; }); - it('has no validator', () => { - expect(result).to.have.validator(null); - }); - - it('has no real type', () => { - expect(result).to.have.realType(null); - }); - - it('has no expected type', () => { - expect(result).to.have.expectedType(null); + it('has "text" validator', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { diff --git a/test/unit/units/validateMethod.test.js b/test/unit/units/validateMethod.test.js index 8f206131..30f01d6e 100644 --- a/test/unit/units/validateMethod.test.js +++ b/test/unit/units/validateMethod.test.js @@ -16,16 +16,8 @@ describe('validateMethod', () => { expect(result).to.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.method" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.method'); - }); - - it('has "text/vnd.apiary.method" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.method'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -47,16 +39,8 @@ describe('validateMethod', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.method" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.method'); - }); - - it('has "text/vnd.apiary.method" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.method'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -71,6 +55,15 @@ describe('validateMethod', () => { 'Expected "method" field to equal "POST", but got "GET".' ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: 'POST', + actual: 'GET' + }); + }); }); }); @@ -88,16 +81,8 @@ describe('validateMethod', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.method" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.method'); - }); - - it('has "text/vnd.apiary.method" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.method'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -110,6 +95,15 @@ describe('validateMethod', () => { .to.have.errorAtIndex(0) .withMessage('Expected "method" field to equal "PATCH", but got "".'); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: 'PATCH', + actual: '' + }); + }); }); }); }); diff --git a/test/unit/units/validateStatusCode.test.js b/test/unit/units/validateStatusCode.test.js index 5985de62..ec2c1932 100644 --- a/test/unit/units/validateStatusCode.test.js +++ b/test/unit/units/validateStatusCode.test.js @@ -16,16 +16,8 @@ describe('validateStatusCode', () => { expect(result).to.be.valid; }); - it('has "TextDiff" validator', () => { - expect(result).to.have.validator('TextDiff'); - }); - - it('has "text/vnd.apiary.status-code" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.status-code'); - }); - - it('has "text/vnd.apiary.status-code" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.status-code'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -47,16 +39,8 @@ describe('validateStatusCode', () => { expect(result).to.not.be.valid; }); - it('has "TextDiff" validator', () => { - expect(result).to.have.validator('TextDiff'); - }); - - it('has "text/vnd.apiary.status-code" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.status-code'); - }); - - it('has "text/vnd.apiary.status-code" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.status-code'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces error', () => { @@ -69,6 +53,15 @@ describe('validateStatusCode', () => { .to.have.errorAtIndex(0) .withMessage(`Status code is '200' instead of '400'`); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '400', + actual: '200' + }); + }); }); }); }); diff --git a/test/unit/units/validateURI.test.js b/test/unit/units/validateURI.test.js index 27f142b4..99def358 100644 --- a/test/unit/units/validateURI.test.js +++ b/test/unit/units/validateURI.test.js @@ -17,16 +17,8 @@ describe('validateURI', () => { expect(result).to.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -49,16 +41,8 @@ describe('validateURI', () => { expect(result).to.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -81,16 +65,8 @@ describe('validateURI', () => { expect(result).to.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -113,16 +89,8 @@ describe('validateURI', () => { expect(result).to.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); it('has no errors', () => { @@ -147,16 +115,8 @@ describe('validateURI', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -171,6 +131,15 @@ describe('validateURI', () => { 'Expected "uri" field to equal "/dashboard", but got: "/profile".' ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/dashboard', + actual: '/profile' + }); + }); }); }); @@ -189,16 +158,8 @@ describe('validateURI', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -213,6 +174,15 @@ describe('validateURI', () => { 'Expected "uri" field to equal "/account?id=123", but got: "/account".' ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/account?id=123', + actual: '/account' + }); + }); }); }); @@ -230,16 +200,8 @@ describe('validateURI', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -254,6 +216,15 @@ describe('validateURI', () => { 'Expected "uri" field to equal "/account?name=user", but got: "/account?nAmE=usEr".' ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/account?name=user', + actual: '/account?nAmE=usEr' + }); + }); }); }); @@ -271,16 +242,8 @@ describe('validateURI', () => { expect(result).to.not.be.valid; }); - it('has "null" validator', () => { - expect(result).to.have.validator(null); - }); - - it('has "text/vnd.apiary.uri" real type', () => { - expect(result).to.have.realType('text/vnd.apiary.uri'); - }); - - it('has "text/vnd.apiary.uri" expected type', () => { - expect(result).to.have.expectedType('text/vnd.apiary.uri'); + it('has "text" kind', () => { + expect(result).to.have.kind('text'); }); describe('produces an error', () => { @@ -295,6 +258,15 @@ describe('validateURI', () => { 'Expected "uri" field to equal "/zoo?type=cats&type=dogs", but got: "/zoo?type=dogs&type=cats".' ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/zoo?type=cats&type=dogs', + actual: '/zoo?type=dogs&type=cats' + }); + }); }); }); }); From a92160fef170488bd204158aa2e5c681dd8d9c45 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 1 Jul 2019 11:23:05 +0200 Subject: [PATCH 2/6] test: adjusts Cucumber step definitions to the new vocabulary --- lib/units/isValid.js | 6 +- lib/units/validateBody.js | 21 +-- lib/units/validateHeaders.js | 20 ++- lib/units/validateMethod.js | 18 +-- lib/units/validateStatusCode.js | 16 ++- lib/units/validateURI.js | 18 +-- lib/validate.js | 24 ++-- scripts/cucumber.js | 3 +- .../cucumber/step_definitions/cli_stepdefs.js | 20 ++- .../step_definitions/validators_steps.js | 20 --- test/cucumber/steps/cli.js | 22 +++ test/cucumber/steps/fields.js | 13 ++ test/cucumber/steps/general.js | 102 +++++++++++++ test/cucumber/support/world.js | 134 +++++++++++++----- .../validateBody/getBodyValidator.test.js | 21 --- 15 files changed, 310 insertions(+), 148 deletions(-) create mode 100644 test/cucumber/steps/cli.js create mode 100644 test/cucumber/steps/fields.js create mode 100644 test/cucumber/steps/general.js diff --git a/lib/units/isValid.js b/lib/units/isValid.js index 6e427cd1..d5a84fab 100644 --- a/lib/units/isValid.js +++ b/lib/units/isValid.js @@ -10,11 +10,11 @@ function isValidField({ errors }) { /** * Returns a boolean indicating the given validation result as valid. - * @param {Object} validationResult + * @param {Object} fields * @returns {boolean} */ -function isValidResult(validationResult) { - return Object.values(validationResult.fields).every(isValidField); +function isValidResult(fields) { + return Object.values(fields).every(isValidField); } module.exports = { diff --git a/lib/units/validateBody.js b/lib/units/validateBody.js index b7ff5ad3..3ca2ad8b 100644 --- a/lib/units/validateBody.js +++ b/lib/units/validateBody.js @@ -140,9 +140,9 @@ function getBodyValidator(realType, expectedType) { }); if (!validator) { - const error = `Can't validate real media type '${mediaTyper.format( + const error = `Can't validate actual media type '${mediaTyper.format( realType - )}' against expected media type '${mediaTyper.format(expectedType)}'.`; + )}' against the expected media type '${mediaTyper.format(expectedType)}'.`; return [error, null, null]; } @@ -154,14 +154,14 @@ function getBodyValidator(realType, expectedType) { * @param {Object} expected * @param {Object} real */ -function validateBody(expected, real) { - const errors = []; - const realBodyType = typeof real.body; - const hasEmptyRealBody = real.body === ''; +function validateBody(expected, actual) { const values = { expected: expected.body, - actual: real.body + actual: actual.body }; + const errors = []; + const realBodyType = typeof actual.body; + const hasEmptyRealBody = actual.body === ''; // Throw when user input for real body is not a string. if (realBodyType !== 'string') { @@ -171,8 +171,8 @@ function validateBody(expected, real) { } const [realTypeError, realType] = getBodyType( - real.body, - real.headers && real.headers['content-type'], + actual.body, + actual.headers && actual.headers['content-type'], 'real' ); const [expectedTypeError, expectedType] = expected.bodySchema @@ -230,7 +230,7 @@ function validateBody(expected, real) { const validator = ValidatorClass && new ValidatorClass( - real.body, + actual.body, usesJsonSchema ? expected.bodySchema : expected.body ); // Without ".validate()" it cannot evaluate output to result. @@ -242,6 +242,7 @@ function validateBody(expected, real) { return { valid: isValidField({ errors }), kind, + values, errors }; } diff --git a/lib/units/validateHeaders.js b/lib/units/validateHeaders.js index 4ca8ebc8..20c6bbe9 100644 --- a/lib/units/validateHeaders.js +++ b/lib/units/validateHeaders.js @@ -14,17 +14,21 @@ function getHeadersType(headers) { * @param {Object} expected * @param {Object} real */ -function validateHeaders(expected, real) { - const expectedType = getHeadersType(expected.headers); - const realType = getHeadersType(real.headers); +function validateHeaders(expected, actual) { + const values = { + expected: expected.headers, + actual: actual.headers + }; + const expectedType = getHeadersType(values.expected); + const actualType = getHeadersType(values.actual); const errors = []; const hasJsonHeaders = - realType === APIARY_JSON_HEADER_TYPE && + actualType === APIARY_JSON_HEADER_TYPE && expectedType === APIARY_JSON_HEADER_TYPE; const validator = hasJsonHeaders - ? new HeadersJsonExample(real.headers, expected.headers) + ? new HeadersJsonExample(values.actual, values.expected) : null; // if you don't call ".validate()", it never evaluates any results. @@ -36,16 +40,18 @@ function validateHeaders(expected, real) { errors.push({ message: `\ No validator found for real data media type -"${realType}" +"${actualType}" and expected data media type "${expectedType}".\ -` +`, + values }); } return { valid: isValidField({ errors }), kind: hasJsonHeaders ? 'json' : 'text', + values, errors }; } diff --git a/lib/units/validateMethod.js b/lib/units/validateMethod.js index 1390b792..ba30ef09 100644 --- a/lib/units/validateMethod.js +++ b/lib/units/validateMethod.js @@ -1,22 +1,22 @@ -function validateMethod(expected, real) { - const { method: expectedMethod } = expected; - const { method: realMethod } = real; - const valid = realMethod === expectedMethod; +function validateMethod(expected, actual) { + const values = { + expected: expected.method, + actual: actual.method + }; + const valid = values.actual === values.expected; const errors = []; if (!valid) { errors.push({ - message: `Expected "method" field to equal "${expectedMethod}", but got "${realMethod}".`, - values: { - expected: expectedMethod, - actual: realMethod - } + message: `Expected method '${values.expected}', but got '${values.actual}'.`, + values }); } return { valid, kind: 'text', + values, errors }; } diff --git a/lib/units/validateStatusCode.js b/lib/units/validateStatusCode.js index 36d5ac2a..9d867dd9 100644 --- a/lib/units/validateStatusCode.js +++ b/lib/units/validateStatusCode.js @@ -3,23 +3,25 @@ * @param {Object} real * @param {number} expected */ -function validateStatusCode(expected, real) { - const valid = real.statusCode === expected.statusCode; +function validateStatusCode(expected, actual) { + const values = { + expected: expected.statusCode, + actual: actual.statusCode + }; + const valid = values.actual === values.expected; const errors = []; if (!valid) { errors.push({ - message: `Status code is '${real.statusCode}' instead of '${expected.statusCode}'`, - values: { - expected: expected.statusCode, - actual: real.statusCode - } + message: `Expected status code '${values.expected}', but got '${values.actual}'.`, + values }); } return { valid, kind: 'text', + values, errors }; } diff --git a/lib/units/validateURI.js b/lib/units/validateURI.js index cdb4eb6b..8c5a6cb9 100644 --- a/lib/units/validateURI.js +++ b/lib/units/validateURI.js @@ -19,14 +19,16 @@ const parseURI = (uri) => { }; const validateURI = (expected, actual) => { - const { uri: expectedURI } = expected; - const { uri: actualURI } = actual; + const values = { + expected: expected.uri, + actual: actual.uri + }; // Parses URI to perform a correct comparison: // - literal comparison of pathname // - order-insensitive comparison of query parameters - const parsedExpected = parseURI(expectedURI); - const parsedActual = parseURI(actualURI); + const parsedExpected = parseURI(values.expected); + const parsedActual = parseURI(values.actual); // Note the different order of arguments between // "validateURI" and "deepEqual". @@ -35,17 +37,15 @@ const validateURI = (expected, actual) => { if (!valid) { errors.push({ - message: `Expected "uri" field to equal "${expectedURI}", but got: "${actualURI}".`, - values: { - expected: expectedURI, - actual: actualURI - } + message: `Expected URI '${values.expected}', but got '${values.actual}'.`, + values }); } return { valid, kind: 'text', + values, errors }; }; diff --git a/lib/validate.js b/lib/validate.js index 74e9a15e..5272c311 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -8,16 +8,15 @@ const { validateStatusCode } = require('./units/validateStatusCode'); const { validateHeaders } = require('./units/validateHeaders'); const { validateBody } = require('./units/validateBody'); -function validate(expectedMessage, realMessage) { - const result = { - fields: {} - }; +function validate(expectedMessage, actualMessage) { + const result = {}; + const fields = {}; // Uses strict coercion on real message. // Strict coercion ensures that real message always has properties // illegible for validation with the expected message, even if they // are not present in the real message. - const real = normalize(coerce(realMessage)); + const actual = normalize(coerce(actualMessage)); // Use weak coercion on expected message. // Weak coercion will transform only the properties present in the @@ -27,27 +26,28 @@ function validate(expectedMessage, realMessage) { const expected = normalize(coerceWeak(expectedMessage)); if (expected.method) { - result.fields.method = validateMethod(expected, real); + fields.method = validateMethod(expected, actual); } if (expected.uri) { - result.fields.uri = validateURI(expected, real); + fields.uri = validateURI(expected, actual); } if (expected.statusCode) { - result.fields.statusCode = validateStatusCode(expected, real); + fields.statusCode = validateStatusCode(expected, actual); } if (expected.headers) { - result.fields.headers = validateHeaders(expected, real); + fields.headers = validateHeaders(expected, actual); } if (isset(expected.body) || isset(expected.bodySchema)) { - result.fields.body = validateBody(expected, real); + fields.body = validateBody(expected, actual); } - // Indicates the validity of the real message - result.valid = isValidResult(result); + // Indicates the validity of the actual message + result.valid = isValidResult(fields); + result.fields = fields; return result; } diff --git a/scripts/cucumber.js b/scripts/cucumber.js index 4109a69d..ec6271f9 100644 --- a/scripts/cucumber.js +++ b/scripts/cucumber.js @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ const spawn = require('cross-spawn'); const isWindows = process.platform.match(/^win/); @@ -22,7 +23,7 @@ const cucumber = spawn( '-r', 'test/cucumber/support/', '-r', - 'test/cucumber/step_definitions/', + 'test/cucumber/steps/', '-f', 'pretty', 'node_modules/gavel-spec/features/' diff --git a/test/cucumber/step_definitions/cli_stepdefs.js b/test/cucumber/step_definitions/cli_stepdefs.js index 1feec0ec..2c481e59 100644 --- a/test/cucumber/step_definitions/cli_stepdefs.js +++ b/test/cucumber/step_definitions/cli_stepdefs.js @@ -9,7 +9,7 @@ module.exports = function() { return callback(); }); - this.Given(/^you record real raw HTTP messages:$/, function(cmd, callback) { + this.Given(/^you record actual raw HTTP messages:$/, function(cmd, callback) { this.commandBuffer += `;${cmd}`; return callback(); }); @@ -22,7 +22,10 @@ module.exports = function() { } ); - this.When(/^a header is missing in real messages:$/, function(cmd, callback) { + this.When(/^a header is missing in actual messages:$/, function( + cmd, + callback + ) { this.commandBuffer += `;${cmd}`; return callback(); }); @@ -36,16 +39,11 @@ module.exports = function() { }`; const child = exec(cmd, function(error, stdout, stderr) { if (error) { - if (parseInt(error.code) !== parseInt(expectedExitStatus)) { + if (parseInt(error.code, 10) !== parseInt(expectedExitStatus, 10)) { return callback( new Error( - `Expected exit status ${expectedExitStatus} but got ${ - error.code - }.` + - 'STDERR: ' + - stderr + - 'STDOUT: ' + - stdout + `Expected exit status ${expectedExitStatus} but got ${error.code}. STDERR: ${stderr} + STDOUT: ${stdout}` ) ); } @@ -55,7 +53,7 @@ module.exports = function() { }); return child.on('exit', function(code) { - if (parseInt(code) !== parseInt(expectedExitStatus)) { + if (parseInt(code, 10) !== parseInt(expectedExitStatus, 10)) { callback( new Error( `Expected exit status ${expectedExitStatus} but got ${code}.` diff --git a/test/cucumber/step_definitions/validators_steps.js b/test/cucumber/step_definitions/validators_steps.js index 7eb10a60..b8bf3a25 100644 --- a/test/cucumber/step_definitions/validators_steps.js +++ b/test/cucumber/step_definitions/validators_steps.js @@ -29,7 +29,6 @@ module.exports = function() { try { const result = this.validate(); this.results = JSON.parse(JSON.stringify(result)); - this.booleanResult = result.isValid; return callback(); } catch (error) { callback(new Error(`Got error during validation:\n${error}`)); @@ -170,25 +169,6 @@ module.exports = function() { } }); - this.Then(/^validator "([^"]*)" is used for validation$/, function( - validator, - callback - ) { - const usedValidator = this.componentResults.validator; - if (validator !== usedValidator) { - callback( - new Error( - `Used validator '${usedValidator}'` + - " instead of '" + - validator + - "'. Got validation results: " + - JSON.stringify(this.results, null, 2) - ) - ); - } - return callback(); - }); - this.Then( /^validation key "([^"]*)" looks like the following "([^"]*)":$/, function(key, type, expected, callback) { diff --git a/test/cucumber/steps/cli.js b/test/cucumber/steps/cli.js new file mode 100644 index 00000000..1e2ee772 --- /dev/null +++ b/test/cucumber/steps/cli.js @@ -0,0 +1,22 @@ +const { assert } = require('chai'); + +module.exports = function() { + this.Given( + /^(you record (expected|actual) raw HTTP message:)|(a header is missing in actual message:)$/, + function(_, __, ___, command) { + this.commands.push(command); + } + ); + + this.When( + 'you validate the message using the following Gavel command:', + async function(command) { + this.commands.push(command); + this.status = await this.executeCommands(this.commands); + } + ); + + this.Then(/^exit status is (\d+)$/, function(expectedStatus) { + assert.equal(this.status, expectedStatus, 'Process statuses do not match'); + }); +}; diff --git a/test/cucumber/steps/fields.js b/test/cucumber/steps/fields.js new file mode 100644 index 00000000..6c29facb --- /dev/null +++ b/test/cucumber/steps/fields.js @@ -0,0 +1,13 @@ +const chai = require('chai'); +const jhp = require('json-parse-helpfulerror'); + +chai.config.truncateThreshold = 0; +const { expect } = chai; + +module.exports = function() { + this.Then(/^field "([^"]*)" equals:$/, function(fieldName, expectedJson) { + const expected = jhp.parse(expectedJson); + + expect(this.result.fields[fieldName]).to.deep.equal(expected); + }); +}; diff --git a/test/cucumber/steps/general.js b/test/cucumber/steps/general.js new file mode 100644 index 00000000..9b734184 --- /dev/null +++ b/test/cucumber/steps/general.js @@ -0,0 +1,102 @@ +const { expect } = require('chai'); +const Diff = require('googlediff'); +const jhp = require('json-parse-helpfulerror'); + +module.exports = function() { + this.Given( + /^you expect the following HTTP (message|request|response):$/i, + function(_, expectedMessage) { + this.expected = jhp.parse(expectedMessage); + } + ); + + this.Given(/^actual HTTP (message|request|response) is:$/i, function( + _, + actualMessage + ) { + this.actual = jhp.parse(actualMessage); + }); + + this.Given(/^actual "([^"]*)" field equals "([^"]*)"/, function( + fieldName, + value + ) { + this.actual[fieldName] = value; + }); + + this.Given(/^you expect "([^"]*)" field to equal "([^"]*)"$/, function( + fieldName, + expectedValue + ) { + this.expected[fieldName] = expectedValue; + }); + + this.Given(/^you expect "([^"]*)" field to equal:$/, function( + fieldName, + codeBlock + ) { + // Perform conditional code block parsing (if headers, etc.) + this.expected[fieldName] = this.transformCodeBlock(fieldName, codeBlock); + }); + + this.Given( + /^you expect "body" field to match the following "([^"]*)":$/, + function(bodyType, value) { + switch (bodyType) { + case 'JSON schema': + this.expected.bodySchema = value; + break; + default: + this.expected.body = value; + break; + } + } + ); + + this.Given(/^actual "([^"]*)" field equals:$/, function( + fieldName, + codeBlock + ) { + // Also perform conditional code parsing + this.actual[fieldName] = this.transformCodeBlock(fieldName, codeBlock); + }); + + // Actions + this.When('Gavel validates HTTP message', function() { + this.validate(); + }); + + // Assertions + this.Then(/^HTTP message is( NOT)? valid$/i, function(isInvalid) { + expect(this.result).to.have.property('valid', !isInvalid); + }); + + this.Then('the validation result is:', function(expectedResult) { + const dmp = new Diff(); + const stringifiedActual = JSON.stringify(this.result, null, 2); + + expect(this.result).to.deep.equal( + jhp.parse(expectedResult), + `\ +Expected the following result: + +${stringifiedActual} + +to equal: + +${expectedResult} + +See the text diff patches below: + +${dmp.patch_toText(dmp.patch_make(stringifiedActual, expectedResult))} +` + ); + }); + + this.Then(/^field "(\w+)" is( NOT)? valid$/i, function(fieldName, isInvalid) { + expect(this.result).to.have.nested.property( + `fields.${fieldName}.valid`, + !isInvalid + ); + }); +}; diff --git a/test/cucumber/support/world.js b/test/cucumber/support/world.js index 9e70eb4e..225fb4e2 100644 --- a/test/cucumber/support/world.js +++ b/test/cucumber/support/world.js @@ -5,10 +5,12 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ /* eslint-disable */ -const gavel = require('../../../lib'); const vm = require('vm'); +const Diff = require('googlediff'); const util = require('util'); const { assert } = require('chai'); +const { exec } = require('child_process'); +const gavel = require('../../../lib'); const HTTP_LINE_DELIMITER = '\n'; @@ -17,35 +19,27 @@ class World { this.codeBuffer = ''; this.commandBuffer = ''; - // Data for creation of: - // - // - ExpecterHttpResponse - // - ExpectedHttpRequest - // - ExpectedHttpMessage - this.expected = {}; + // NEW + this.commands = []; - // Data for creation of: - // - // - HttpResponse - // - HttpRequest - // - HttpMessage - this.real = {}; + this.expected = {}; + this.actual = {}; // Parsed HTTP objects for model valdiation this.model = {}; - // Results of validators + // Gavel validation result this.results = {}; + // CLI: Process exit status + this.status = null; + // Validation verdict for the whole HTTP Message - this.booleanResult = false; + // this.booleanResult = false; // Component relevant to the expectation, e.g. 'body' this.component = null; this.componentResults = null; - - this.expectedType = null; - this.realType = null; } expectBlockEval(block, expectedReturn, callback) { @@ -93,8 +87,8 @@ class World { // http://nodejs.org/docs/v0.8.23/api/all.html#all_all_together const formattedCode = code.replace( - "require('gavel", - "require('../../../lib" + `require('gavel`, + `require('../../../lib` ); try { @@ -111,31 +105,83 @@ class World { } } + executeCommands(commands) { + const commandsBuffer = commands.join(';'); + const cmd = + `PATH=$PATH:${process.cwd()}/bin:${process.cwd()}/node_modules/.bin; cd /tmp/gavel-* ;` + + commandsBuffer; + + return new Promise((resolve) => { + const child = exec(cmd, function(error, stdout, stderr) { + if (error) { + resolve(error.code); + } + }); + + child.on('exit', function(code) { + resolve(code); + }); + }); + } + validate() { - return gavel.validate(this.expected, this.real); + this.result = gavel.validate(this.expected, this.actual); + } + + transformCodeBlock(fieldName, value) { + switch (fieldName) { + case 'headers': + return this.parseHeaders(value); + default: + return value; + } } parseHeaders(headersString) { const lines = headersString.split(HTTP_LINE_DELIMITER); - const headers = {}; - for (let line of Array.from(lines)) { - const parts = line.split(':'); - const key = parts.shift(); - headers[key.toLowerCase()] = parts.join(':').trim(); - } + + const headers = lines.reduce((acc, line) => { + // Using RegExp to parse a header line. + // Splitting by semicolon (:) would split + // Date header's time delimiter: + // > Date: Fri, 13 Dec 3000 23:59:59 GMT + const match = line.match(/^(\S+):\s+(.+)$/); + + assert.isNotNull( + match, + `\ +Failed to parse a header line: +${line} + +Make sure it's in the "Header-Name: value" format. +` + ); + + const [_, key, value] = match; + + return { + ...acc, + [key.toLowerCase()]: value.trim() + }; + }, {}); return headers; } parseRequestLine(parsed, firstLine) { - firstLine = firstLine.split(' '); - parsed.method = firstLine[0]; - parsed.uri = firstLine[1]; + const [method, uri] = firstLine.split(' '); + parsed.method = method; + parsed.uri = uri; + // firstLine = firstLine.split(' '); + // parsed.method = firstLine[0]; + // parsed.uri = firstLine[1]; } parseResponseLine(parsed, firstLine) { - firstLine = firstLine.split(' '); - parsed.statusCode = firstLine[1]; - parsed.statusMessage = firstLine[2]; + const [statusCode] = firstLine.split(' '); + parsed.statusCode = statusCode; + // firstLine = firstLine.split(' '); + // parsed.statusCode = firstLine[1]; + // parsed.statusMessage = firstLine[2]; } parseHttp(type, string) { @@ -144,7 +190,6 @@ class World { } const parsed = {}; - const lines = string.split(HTTP_LINE_DELIMITER); if (type === 'request') { @@ -157,6 +202,7 @@ class World { const bodyLines = []; const headersLines = []; let bodyEntered = false; + for (let line of Array.from(lines)) { if (line === '') { bodyEntered = true; @@ -177,21 +223,24 @@ class World { // Hacky coercion function to parse expcected Boolean values // from Gherkin feature suites. + // + // TODO Replace with the {boolean} placeholder from the + // next version of Cucumber. toBoolean(string) { if (string === 'true') return true; if (string === 'false') return false; return !!string; } - toCamelCase(input) { - const result = input.replace(/\s([a-z])/g, (strings) => + toCamelCase(string) { + const result = string.replace(/\s([a-z])/g, (strings) => strings[1].toUpperCase() ); return result; } - toPascalCase(input) { - let result = input.replace( + toPascalCase(string) { + let result = string.replace( /(\w)(\w*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase() ); @@ -207,6 +256,15 @@ class World { } } + diff(expected, actual) { + const dmp = new Diff(); + console.log( + dmp.patch_toText( + dmp.patch_make(JSON.stringify(actual), JSON.stringify(expected)) + ) + ); + } + // Debugging helper throw(data) { throw new Error(this.inspect(data)); diff --git a/test/unit/units/validateBody/getBodyValidator.test.js b/test/unit/units/validateBody/getBodyValidator.test.js index a83e7579..9a977444 100644 --- a/test/unit/units/validateBody/getBodyValidator.test.js +++ b/test/unit/units/validateBody/getBodyValidator.test.js @@ -43,25 +43,4 @@ describe('getBodyValidator', () => { }); }); }); - - // TODO Remove or uncomment - // describe('when given unknown media type', () => { - // const unknownContentTypes = [['text/html', 'text/xml']]; - - // unknownContentTypes.forEach((contentTypes) => { - // const [realContentType, expectedContentType] = contentTypes; - // const [real, expected] = getMediaTypes( - // realContentType, - // expectedContentType - // ); - - // describe(`${realContentType} + ${expectedContentType}`, () => { - // const [error, validator] = getBodyValidator(real, expected); - - // it('...', () => { - // console.log({ error, validator }); - // }); - // }); - // }); - // }); }); From 0cef79e4de703275f75d01d0ade235b221366f8f Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 2 Jul 2019 09:51:56 +0200 Subject: [PATCH 3/6] chore: uses "valid" property in request/response (bin) --- bin/gavel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/gavel b/bin/gavel index d7f8e779..be54be16 100755 --- a/bin/gavel +++ b/bin/gavel @@ -33,7 +33,7 @@ process.stdin.on('end', function() { const requestResult = gavel.validate(expectedRequest, realRequest); const responseResult = gavel.validate(expectedResponse, realResponse); - if (requestResult.isValid && responseResult.isValid) { + if (requestResult.valid && responseResult.valid) { process.exit(0); } else { process.exit(1); From a3b1b2f1110fc699c445a6d62544f62c789faf7e Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 2 Jul 2019 10:00:11 +0200 Subject: [PATCH 4/6] chore: removes unused step definitions for cucumber tests --- test/cucumber/step_definitions/body_steps.js | 21 -- .../cucumber/step_definitions/cli_stepdefs.js | 66 ------ .../step_definitions/headers_steps.js | 17 -- .../step_definitions/javascript_steps.js | 54 ----- .../cucumber/step_definitions/method_steps.js | 17 -- test/cucumber/step_definitions/model_steps.js | 57 ----- .../step_definitions/status_code_steps.js | 14 -- test/cucumber/step_definitions/uri_steps.js | 17 -- .../validation_errors_thens.js | 46 ---- .../step_definitions/validators_steps.js | 215 ------------------ 10 files changed, 524 deletions(-) delete mode 100644 test/cucumber/step_definitions/body_steps.js delete mode 100644 test/cucumber/step_definitions/cli_stepdefs.js delete mode 100644 test/cucumber/step_definitions/headers_steps.js delete mode 100644 test/cucumber/step_definitions/javascript_steps.js delete mode 100644 test/cucumber/step_definitions/method_steps.js delete mode 100644 test/cucumber/step_definitions/model_steps.js delete mode 100644 test/cucumber/step_definitions/status_code_steps.js delete mode 100644 test/cucumber/step_definitions/uri_steps.js delete mode 100644 test/cucumber/step_definitions/validation_errors_thens.js delete mode 100644 test/cucumber/step_definitions/validators_steps.js diff --git a/test/cucumber/step_definitions/body_steps.js b/test/cucumber/step_definitions/body_steps.js deleted file mode 100644 index db3b21d0..00000000 --- a/test/cucumber/step_definitions/body_steps.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = function() { - this.Given( - /^you define expected HTTP body using the following "([^"]*)":$/, - function(type, body, callback) { - if (type === 'textual example') { - this.expected.body = body; - } else if (type === 'JSON example') { - this.expected.body = body; - } else if (type === 'JSON schema') { - this.expected.bodySchema = JSON.parse(body); - } - - return callback(); - } - ); - - return this.When(/^real HTTP body is following:$/, function(body, callback) { - this.real.body = body; - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/cli_stepdefs.js b/test/cucumber/step_definitions/cli_stepdefs.js deleted file mode 100644 index 2c481e59..00000000 --- a/test/cucumber/step_definitions/cli_stepdefs.js +++ /dev/null @@ -1,66 +0,0 @@ -const { exec } = require('child_process'); - -module.exports = function() { - this.Given(/^you record expected raw HTTP messages:$/, function( - cmd, - callback - ) { - this.commandBuffer += `;${cmd}`; - return callback(); - }); - - this.Given(/^you record actual raw HTTP messages:$/, function(cmd, callback) { - this.commandBuffer += `;${cmd}`; - return callback(); - }); - - this.When( - /^you validate the message using the following Gavel command:$/, - function(cmd, callback) { - this.commandBuffer += `;${cmd}`; - return callback(); - } - ); - - this.When(/^a header is missing in actual messages:$/, function( - cmd, - callback - ) { - this.commandBuffer += `;${cmd}`; - return callback(); - }); - - return this.Then(/^exit status is (\d+)$/, function( - expectedExitStatus, - callback - ) { - const cmd = `PATH=$PATH:${process.cwd()}/bin:${process.cwd()}/node_modules/.bin; cd /tmp/gavel-* ${ - this.commandBuffer - }`; - const child = exec(cmd, function(error, stdout, stderr) { - if (error) { - if (parseInt(error.code, 10) !== parseInt(expectedExitStatus, 10)) { - return callback( - new Error( - `Expected exit status ${expectedExitStatus} but got ${error.code}. STDERR: ${stderr} - STDOUT: ${stdout}` - ) - ); - } - - return callback(); - } - }); - - return child.on('exit', function(code) { - if (parseInt(code, 10) !== parseInt(expectedExitStatus, 10)) { - callback( - new Error( - `Expected exit status ${expectedExitStatus} but got ${code}.` - ) - ); - } - return callback(); - }); - }); -}; diff --git a/test/cucumber/step_definitions/headers_steps.js b/test/cucumber/step_definitions/headers_steps.js deleted file mode 100644 index 952f1a3b..00000000 --- a/test/cucumber/step_definitions/headers_steps.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = function() { - this.Given(/^you expect the following HTTP headers:$/, function( - string, - callback - ) { - this.expected.headers = this.parseHeaders(string); - return callback(); - }); - - return this.When(/^real HTTP headers are following:$/, function( - string, - callback - ) { - this.real.headers = this.parseHeaders(string); - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/javascript_steps.js b/test/cucumber/step_definitions/javascript_steps.js deleted file mode 100644 index 7fb8314f..00000000 --- a/test/cucumber/step_definitions/javascript_steps.js +++ /dev/null @@ -1,54 +0,0 @@ -/* eslint-disable */ -module.exports = function() { - this.Given( - /^you define following( expected)? HTTP (request|response) object:/, - function(isExpected, messageType, string, callback) { - this.codeBuffer += `${string}\n`; - return callback(); - } - ); - - // - this.Given(/^you define the following "([^"]*)" variable:$/, function( - arg1, - string, - callback - ) { - this.codeBuffer += string + '\n'; - return callback(); - }); - - this.Given(/^you add expected "([^"]*)" to real "([^"]*)":$/, function( - arg1, - arg2, - string, - callback - ) { - this.codeBuffer += string + '\n'; - return callback(); - }); - - this.Given(/^prepare result variable:$/, function(string, callback) { - this.codeBuffer += string + '\n'; - return callback(); - }); - - this.Then(/^"([^"]*)" variable will contain:$/, function( - varName, - string, - callback - ) { - this.codeBuffer += varName + '\n'; - const expected = string; - return this.expectBlockEval(this.codeBuffer, expected, callback); - }); - - this.When(/^you call:$/, function(string, callback) { - this.codeBuffer += string + '\n'; - return callback(); - }); - - return this.Then(/^it will return:$/, function(expected, callback) { - return this.expectBlockEval(this.codeBuffer, expected, callback); - }); -}; diff --git a/test/cucumber/step_definitions/method_steps.js b/test/cucumber/step_definitions/method_steps.js deleted file mode 100644 index 3d6a6cfd..00000000 --- a/test/cucumber/step_definitions/method_steps.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = function() { - this.Given(/^you expect HTTP message method "([^"]*)"$/, function( - method, - callback - ) { - this.expected.method = method; - return callback(); - }); - - return this.When(/^real HTTP message method is "([^"]*)"$/, function( - method, - callback - ) { - this.real.method = method; - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/model_steps.js b/test/cucumber/step_definitions/model_steps.js deleted file mode 100644 index e21f6dac..00000000 --- a/test/cucumber/step_definitions/model_steps.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable */ -const deepEqual = require('deep-equal'); -const gavel = require('../../../lib'); - -module.exports = function() { - // TODO consider refactoring for for better acceptace testing to separated steps - // i.e. do not use http parsing, use separate steps for body, headers, code, etc... - this.When(/^you have the following real HTTP request:$/, function( - requestString, - callback - ) { - this.model.request = this.parseHttp('request', requestString); - return callback(); - }); - - this.When(/^you have the following real HTTP response:$/, function( - responseString, - callback - ) { - this.model.response = this.parseHttp('response', responseString); - return callback(); - }); - - return this.Then( - /^"([^"]*)" JSON representation will look like this:$/, - function(objectTypeString, string, callback) { - let data; - const expectedObject = JSON.parse(string); - - if (objectTypeString === 'HTTP Request') { - data = this.model.request; - } else if (objectTypeString === 'HTTP Response') { - data = this.model.response; - } else if (objectTypeString === 'Expected HTTP Request') { - data = this.expected; - } else if (objectTypeString === 'Expected HTTP Response') { - data = this.expected; - } - - const jsonizedInstance = JSON.parse(JSON.stringify(data)); - - if (!deepEqual(expectedObject, jsonizedInstance, { strict: true })) { - callback( - new Error( - 'Objects are not equal: ' + - '\nexpected: \n' + - JSON.stringify(expectedObject, null, 2) + - '\njsonized instance: \n' + - JSON.stringify(jsonizedInstance, null, 2) - ) - ); - } - - return callback(); - } - ); -}; diff --git a/test/cucumber/step_definitions/status_code_steps.js b/test/cucumber/step_definitions/status_code_steps.js deleted file mode 100644 index d57ed23a..00000000 --- a/test/cucumber/step_definitions/status_code_steps.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = function() { - this.Given(/^you expect HTTP status code "([^"]*)"$/, function( - code, - callback - ) { - this.expected.statusCode = code; - return callback(); - }); - - return this.When(/^real status code is "([^"]*)"$/, function(code, callback) { - this.real.statusCode = code; - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/uri_steps.js b/test/cucumber/step_definitions/uri_steps.js deleted file mode 100644 index ffb7c1e6..00000000 --- a/test/cucumber/step_definitions/uri_steps.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = function() { - this.Given(/^you expect HTTP message URI "([^"]*)"$/, function( - uri, - callback - ) { - this.expected.uri = uri; - return callback(); - }); - - return this.When(/^real HTTP message URI is "([^"]*)"$/, function( - uri, - callback - ) { - this.real.uri = uri; - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/validation_errors_thens.js b/test/cucumber/step_definitions/validation_errors_thens.js deleted file mode 100644 index 4758af25..00000000 --- a/test/cucumber/step_definitions/validation_errors_thens.js +++ /dev/null @@ -1,46 +0,0 @@ -const { assert } = require('chai'); - -module.exports = function() { - this.Then(/^field "([^"]*)" is( NOT)? valid$/, function( - fieldName, - isNotValid, - callback - ) { - const result = this.validate(); - - assert.property( - result.fields, - fieldName, - `Expected to have "${fieldName}" field in the validation result, but got none.` - ); - - assert.propertyVal( - result.fields[fieldName], - 'isValid', - !isNotValid, - `Expected "result.fields.${fieldName}" to be valid, but it's not.` - ); - - return callback(); - }); - - this.Then(/^Request or Response is NOT valid$/, function(callback) { - const result = this.validate(); - if (result.isValid) { - callback( - new Error('Request or Response is valid and should NOT be valid.') - ); - } - return callback(); - }); - - return this.Then(/^Request or Response is valid$/, function(callback) { - const result = this.validate(); - if (!result.isValid) { - callback( - new Error('Request or Response is NOT valid and should be valid.') - ); - } - return callback(); - }); -}; diff --git a/test/cucumber/step_definitions/validators_steps.js b/test/cucumber/step_definitions/validators_steps.js deleted file mode 100644 index b8bf3a25..00000000 --- a/test/cucumber/step_definitions/validators_steps.js +++ /dev/null @@ -1,215 +0,0 @@ -/* eslint-disable */ -const tv4 = require('tv4'); -const { assert } = require('chai'); -const deepEqual = require('deep-equal'); - -module.exports = function() { - this.When( - /^you perform a failing validation on any validatable HTTP component$/, - function(callback) { - const json1 = '{"a": "b"}'; - const json2 = '{"c": "d"}'; - - this.component = 'body'; - - this.real = { - headers: { - 'content-type': 'application/json' - }, - body: json1 - }; - - this.expected = { - headers: { - 'content-type': 'application/json' - }, - body: json2 - }; - - try { - const result = this.validate(); - this.results = JSON.parse(JSON.stringify(result)); - return callback(); - } catch (error) { - callback(new Error(`Got error during validation:\n${error}`)); - } - } - ); - - this.Then( - /^the validator output for the HTTP component looks like the following JSON:$/, - function(expectedJson, callback) { - const expected = JSON.parse(expectedJson); - const real = this.results.fields[this.component]; - if (!deepEqual(real, expected, { strict: true })) { - return callback( - new Error( - 'Not matched! Expected:\n' + - JSON.stringify(expected, null, 2) + - '\n' + - 'But got:' + - '\n' + - JSON.stringify(real, null, 2) - ) - ); - } else { - return callback(); - } - } - ); - - this.Then(/^validated HTTP component is considered invalid$/, function( - callback - ) { - assert.isFalse(this.booleanResult); - return callback(); - }); - - this.Then( - /^the validator output for the HTTP component is valid against "([^"]*)" model JSON schema:$/, - function(model, schema, callback) { - const valid = tv4.validate( - this.results.fields[this.component], - JSON.parse(schema) - ); - if (!valid) { - return callback( - new Error( - 'Expected no validation errors on schema but got:\n' + - JSON.stringify(tv4.error, null, 2) - ) - ); - } else { - return callback(); - } - } - ); - - this.Then( - /^each result entry under "([^"]*)" key must contain "([^"]*)" key$/, - function(key1, key2, callback) { - const error = this.results.fields[this.component]; - if (error === undefined) { - callback( - new Error( - 'Validation result for "' + - this.component + - '" is undefined. Validations: ' + - JSON.stringify(this.results, null, 2) - ) - ); - } - - error[key1].forEach((error) => assert.include(Object.keys(error), key2)); - return callback(); - } - ); - - this.Then( - /^the output JSON contains key "([^"]*)" with one of the following values:$/, - function(key, table, callback) { - const error = this.results.fields[this.component]; - - const validators = [].concat.apply([], table.raw()); - - assert.include(validators, error[key]); - return callback(); - } - ); - - this.Given(/^you want validate "([^"]*)" HTTP component$/, function( - component, - callback - ) { - this.component = component; - return callback(); - }); - - this.Given( - /^you express expected data by the following "([^"]*)" example:$/, - function(type, data, callback) { - if (type === 'application/schema+json') { - this.expected['bodySchema'] = data; - } else if (type === 'application/vnd.apiary.http-headers+json') { - this.expected[this.component] = JSON.parse(data); - } else { - this.expected[this.component] = data; - } - - this.expectedType = type; - return callback(); - } - ); - - this.Given(/^you have the following "([^"]*)" real data:$/, function( - type, - data, - callback - ) { - if (type === 'application/vnd.apiary.http-headers+json') { - this.real[this.component] = JSON.parse(data); - } else { - this.real[this.component] = data; - } - - this.realType = type; - return callback(); - }); - - this.When(/^you perform validation on the HTTP component$/, function( - callback - ) { - try { - const result = this.validate(); - this.results = result; - this.componentResults = this.results.fields[this.component]; - return callback(); - } catch (error) { - callback(new Error(`Error during validation: ${error}`)); - } - }); - - this.Then( - /^validation key "([^"]*)" looks like the following "([^"]*)":$/, - function(key, type, expected, callback) { - const real = this.componentResults[key]; - if (type === 'JSON') { - expected = JSON.parse(expected); - } else if (type === 'text') { - // FIXME investigate how does cucumber docstrings handle - // newlines and remove trim and remove this hack - expected = expected + '\n'; - } - - if (type === 'JSON') { - if (!deepEqual(expected, real, { strict: true })) { - callback( - new Error( - 'Not matched! Expected:\n' + - this.inspect(expected) + - '\n' + - 'But got:' + - '\n' + - this.inspect(real) + - '\n' + - 'End' - ) - ); - } - } else if (type === 'text') { - assert.equal(expected, real); - } - return callback(); - } - ); - - return this.Then(/^each result entry must contain "([^"]*)" key$/, function( - key, - callback - ) { - this.componentResults.errors.forEach((error) => - assert.include(Object.keys(error), key) - ); - return callback(); - }); -}; From 7ebc99234ae1dc8c084886aa865ec3fa604b4628 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 2 Jul 2019 10:07:04 +0200 Subject: [PATCH 5/6] test: adjusts test suites to use unified error message format --- test/chai.js | 2 +- test/integration/validate.test.js | 12 ++++-------- test/unit/units/validateBody.test.js | 2 +- test/unit/units/validateMethod.test.js | 6 ++---- test/unit/units/validateStatusCode.test.js | 2 +- test/unit/units/validateURI.test.js | 10 ++++------ 6 files changed, 13 insertions(+), 21 deletions(-) diff --git a/test/chai.js b/test/chai.js index 2e18dafa..1deb78d9 100644 --- a/test/chai.js +++ b/test/chai.js @@ -44,8 +44,8 @@ to have an error at index ${currentErrorIndex} that includes property "${propNam ${JSON.stringify(target)} `, - JSON.stringify(expectedValue), JSON.stringify(target), + JSON.stringify(expectedValue), true ); }); diff --git a/test/integration/validate.test.js b/test/integration/validate.test.js index 639407ef..255a6244 100644 --- a/test/integration/validate.test.js +++ b/test/integration/validate.test.js @@ -74,9 +74,7 @@ describe('validate', () => { it('has explanatory message', () => { expect(result.fields.method) .to.have.errorAtIndex(0) - .withMessage( - 'Expected "method" field to equal "PUT", but got "POST".' - ); + .withMessage(`Expected method 'PUT', but got 'POST'.`); }); }); }); @@ -176,7 +174,7 @@ describe('validate', () => { it('has explanatory message', () => { expect(result.fields.statusCode) .to.have.errorAtIndex(0) - .withMessage(`Status code is '400' instead of '200'`); + .withMessage(`Expected status code '200', but got '400'.`); }); }); }); @@ -294,9 +292,7 @@ describe('validate', () => { it('has explanatory message', () => { expect(result.fields.method) .to.have.errorAtIndex(0) - .withMessage( - 'Expected "method" field to equal "POST", but got "PUT".' - ); + .withMessage(`Expected method 'POST', but got 'PUT'.`); }); it('includes values', () => { @@ -323,7 +319,7 @@ describe('validate', () => { it('has explanatory message', () => { expect(result.fields.statusCode) .to.have.errorAtIndex(0) - .withMessage(`Status code is 'undefined' instead of '200'`); + .withMessage(`Expected status code '200', but got 'undefined'.`); }); }); }); diff --git a/test/unit/units/validateBody.test.js b/test/unit/units/validateBody.test.js index 7373c426..27b89f85 100644 --- a/test/unit/units/validateBody.test.js +++ b/test/unit/units/validateBody.test.js @@ -48,7 +48,7 @@ describe('validateBody', () => { expect(result) .to.have.errorAtIndex(0) .withMessage( - `Can't validate real media type 'application/json' against expected media type 'text/plain'.` + `Can't validate actual media type 'application/json' against the expected media type 'text/plain'.` ); }); diff --git a/test/unit/units/validateMethod.test.js b/test/unit/units/validateMethod.test.js index 30f01d6e..d5629846 100644 --- a/test/unit/units/validateMethod.test.js +++ b/test/unit/units/validateMethod.test.js @@ -51,9 +51,7 @@ describe('validateMethod', () => { it('has explanatory message', () => { expect(result) .to.have.errorAtIndex(0) - .withMessage( - 'Expected "method" field to equal "POST", but got "GET".' - ); + .withMessage(`Expected method 'POST', but got 'GET'.`); }); it('includes values', () => { @@ -93,7 +91,7 @@ describe('validateMethod', () => { it('has explanatory message', () => { expect(result) .to.have.errorAtIndex(0) - .withMessage('Expected "method" field to equal "PATCH", but got "".'); + .withMessage(`Expected method 'PATCH', but got ''.`); }); it('includes values', () => { diff --git a/test/unit/units/validateStatusCode.test.js b/test/unit/units/validateStatusCode.test.js index ec2c1932..d510ed7c 100644 --- a/test/unit/units/validateStatusCode.test.js +++ b/test/unit/units/validateStatusCode.test.js @@ -51,7 +51,7 @@ describe('validateStatusCode', () => { it('has explanatory message', () => { expect(result) .to.have.errorAtIndex(0) - .withMessage(`Status code is '200' instead of '400'`); + .withMessage(`Expected status code '400', but got '200'.`); }); it('includes values', () => { diff --git a/test/unit/units/validateURI.test.js b/test/unit/units/validateURI.test.js index 99def358..de706cba 100644 --- a/test/unit/units/validateURI.test.js +++ b/test/unit/units/validateURI.test.js @@ -127,9 +127,7 @@ describe('validateURI', () => { it('has explanatory message', () => { expect(result) .to.have.errorAtIndex(0) - .withMessage( - 'Expected "uri" field to equal "/dashboard", but got: "/profile".' - ); + .withMessage(`Expected URI '/dashboard', but got '/profile'.`); }); it('includes values', () => { @@ -171,7 +169,7 @@ describe('validateURI', () => { expect(result) .to.have.errorAtIndex(0) .withMessage( - 'Expected "uri" field to equal "/account?id=123", but got: "/account".' + `Expected URI '/account?id=123', but got '/account'.` ); }); @@ -213,7 +211,7 @@ describe('validateURI', () => { expect(result) .to.have.errorAtIndex(0) .withMessage( - 'Expected "uri" field to equal "/account?name=user", but got: "/account?nAmE=usEr".' + `Expected URI '/account?name=user', but got '/account?nAmE=usEr'.` ); }); @@ -255,7 +253,7 @@ describe('validateURI', () => { expect(result) .to.have.errorAtIndex(0) .withMessage( - 'Expected "uri" field to equal "/zoo?type=cats&type=dogs", but got: "/zoo?type=dogs&type=cats".' + `Expected URI '/zoo?type=cats&type=dogs', but got '/zoo?type=dogs&type=cats'.` ); }); From 8bc0807da80fccdbd2e39d124c2cd54a58fc19ed Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 2 Jul 2019 11:29:56 +0200 Subject: [PATCH 6/6] chore: updates readme with the new API --- README.md | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 212 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 10e04b77..63ff44b0 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,225 @@ -# Gavel.js — Validator of HTTP Transactions - [![npm version](https://badge.fury.io/js/gavel.svg)](https://badge.fury.io/js/gavel) [![Build Status](https://travis-ci.org/apiaryio/gavel.js.svg?branch=master)](https://travis-ci.org/apiaryio/gavel.js) [![Build status](https://ci.appveyor.com/api/projects/status/0cpnaoakhs8q58tn/branch/master?svg=true)](https://ci.appveyor.com/project/Apiary/gavel-js/branch/master) -[![Dependency Status](https://david-dm.org/apiaryio/gavel.js.svg)](https://david-dm.org/apiaryio/gavel.js) -[![devDependency Status](https://david-dm.org/apiaryio/gavel.js/dev-status.svg)](https://david-dm.org/apiaryio/gavel.js#info=devDependencies) -[![Greenkeeper badge](https://badges.greenkeeper.io/apiaryio/gavel.js.svg)](https://greenkeeper.io/) [![Coverage Status](https://coveralls.io/repos/apiaryio/gavel.js/badge.svg?branch=master)](https://coveralls.io/r/apiaryio/gavel.js?branch=master) [![Known Vulnerabilities](https://snyk.io/test/npm/gavel/badge.svg)](https://snyk.io/test/npm/gavel) -![Gavel.js - Validator of HTTP Transactions](https://raw.github.com/apiaryio/gavel/master/img/gavel.png?v=1) +
+ +

+ Gavel logo +

+ +

Gavel

-Gavel detects important differences between actual and expected HTTP transactions (HTTP request and response pairs). Gavel also decides whether the actual HTTP transaction is valid or not. +Gavel is a library that validates a given HTTP transaction (request/response pair) and returns its verdict. -## Installation +## Install -```sh -$ npm install gavel +```bash +npm install gavel ``` -## Documentation +## Usage + +### CLI + +```bash +# (Optional) Record HTTP messages +curl -s --trace - http://httpbin.org/ip | curl-trace-parser > expected +curl -s --trace - http://httpbin.org/ip | curl-trace-parser > actual + +# Perform the validation +cat actual | gavel expected +``` + +> Example above uses [`curl-trace-parser`](https://github.com/apiaryio/curl-trace-parser). + +### NodeJS + +```js +const gavel = require('gavel'); + +// Define HTTP messages +const expected = { + statusCode: 200, + headers: { + 'Content-Type': 'application/json' + } +}; + +const actual = { + statusCode: 404, + headers: { + 'Content-Type': 'application/json' + } +}; + +// Perform the validation +const result = gavel.validate(expected, actual); +``` + +Executing the code above returns the next validation `result`: + +```js +{ + valid: false, + fields: { + statusCode: { + valid: false, + kind: 'text', + values: { + expected: 200, + actual: 404 + }, + errors: [ + { + message: `Expected status code '200', but got '404'.`, + values: { + expected: 200, + actual: 404 + } + } + ] + }, + headers: { + valid: true, + kind: 'json', + values: { + expected: { + 'Content-Type': 'application/json' + }, + actual: { + 'Content-Type': 'application/json' + } + }, + errors: [] + } + } +} +``` + +### Usage with JSON Schema + +You can describe the body expectations using [JSON Schema](https://json-schema.org/) by providing a valid schema to the `bodySchema` property of the expected HTTP message: + +```js +const gavel = require('gavel'); + +const expected = { + bodySchema: { + type: 'object', + properties: { + fruits: { + type: 'array', + items: { + type: 'string' + } + } + } + } +}; + +const actual = { + body: JSON.stringify({ + fruits: ['apple', 'banana', 2] + }) +}; + +const result = gavel.validate(expected, actual); +``` + +The validation `result` against the given JSON Schema will look as follows: + +```js +{ + valid: false, + fields: { + body: { + valid: false, + kind: 'json', + values: { + actual: "{\"fruits\":[\"apple\",\"banana\",2]}" + }, + errors: [ + { + message: `At '/fruits/2' Invalid type: number (expected string)`, + location: { + pointer: '/fruits/2' + } + } + ] + } + } +} +``` + +## Type definitions + +### Input + +> Gavel has no assumptions over the validity of a given HTTP message. It is the end user responsibility to operate on the valid data. + +Both expected and actual HTTP messages inherit from a generic `HttpMessage` interface: + +```ts +interface HttpMessage { + method?: string; + statusCode?: number; + headers?: Record | string; + body?: string; + bodySchema?: Object | string; // string containing a valid JSON schema +} +``` + +Gavel will throw an exception when given invalid input data. + +### Output + +```ts +// Field kind describes the type of a field's values +// subjected to the end comparison. +enum FieldKind { + null // validation didn't happen (non-comparable data) + text // compared as text + json // compared as JSON +} + +interface ValidationResult { + valid: boolean // validity of the actual message + fields: { + [fieldName: string]: { + valid: boolean // validity of a single field + kind: FieldKind + values: { // end compared values (coerced, normalized) + actual: any + expected: any + } + errors: FieldError[] + } + } +} + +interface FieldError { + message: string + // Additional error information. + // Location is kind-specific. + location?: { + // JSON + pointer?: string + property?: string + } + values?: { + expected: any + actual: any + } +} +``` + +## API + +- `validate(expected: HttpMessage, actual: HttpMessage): ValidationResult` -Gavel.js is a JavaScript implementation of the [Gavel behavior specification](https://www.relishapp.com/apiary/gavel/) ([repository](https://github.com/apiaryio/gavel-spec)): +## License -- [Gavel.js-specific documentation](https://www.relishapp.com/apiary/gavel/docs/javascript/) -- [CLI documentation](https://www.relishapp.com/apiary/gavel/docs/command-line-interface/) +MIT