diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..368fe859 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v8.12.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..62af5cf7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +Thank you for deciding to contribute to Gavel.js. Please read the guidelines below to ensure the smoothest developer's experience during the involvement. + +## Workflow + +1. Clone the repository. +2. Install dependencies: + +```bash +npm install +``` + +3. Use the correct NodeJS version: + +```bash +nvm use +``` + +4. Create a branch for a feature or a bugfix. +5. Follow the [Conventional Commit Messages](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#user-content--git-commit-guidelines) for commit messages. +6. Issue a pull request and undergo a code review. +7. Upon approval, merge the pull request. diff --git a/README.md b/README.md index cc02e6cf..da5d41cf 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,249 @@ -# Gavel.js — Validator of HTTP Transactions +

+ + npm version + + + Build Status + + + Build Status + + + Coverage Status + + + Known Vulnerabilities + +

-[![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) -[![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 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

-## Installation +

Gavel tells you whether an actual HTTP message is valid against an expected HTTP message.

-```sh -$ npm install gavel +## Install + +```bash +npm install gavel +``` + +## 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 +``` + +> **Gavel CLI is not supported on Windows**. 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); +``` + +The code above would return the following 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 + +> When a parsable JSON body is expected without an explicit schema the [default schema](https://github.com/apiaryio/gavel-spec/blob/master/features/expectations/bodyJsonExample.feature) is inferred. + +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' + } + } + ] + } + } +} +``` + +> Note that JSON schema Draft-05+ are not currently supported. [Follow the support progress](https://github.com/apiaryio/gavel.js/issues/90). + +## Examples + +Take a look at the [Gherkin](https://cucumber.io/docs/gherkin/) specification, which describes on examples how validation of each field behaves: + +- [`method`](https://github.com/apiaryio/gavel-spec/blob/master/features/javascript/fields/method) +- [`statusCode`](https://github.com/apiaryio/gavel-spec/blob/master/features/javascript/fields/statusCode) +- [`headers`](https://github.com/apiaryio/gavel-spec/blob/master/features/javascript/fields/headers) +- [`body`](https://github.com/apiaryio/gavel-spec/blob/master/features/javascript/fields/body) +- [`bodySchema`](https://github.com/apiaryio/gavel-spec/blob/master/features/javascript/fields/bodySchema) + +## Type definitions + +Type definitions below are described using [TypeScript](https://www.typescriptlang.org/) syntax. + +### Input + +> Gavel makes no assumptions over the validity of a given HTTP message according to the HTTP specification (RFCs [2616](https://www.ietf.org/rfc/rfc2616.txt), [7540](https://httpwg.org/specs/rfc7540.html)) and will accept any input matching the input type definition. Gavel will throw an exception when given malformed input data. + +Both expected and actual HTTP messages (no matter request or response) inherit from a single `HttpMessage` interface: + +```ts +interface HttpMessage { + method?: string; + statusCode?: number; + headers?: Record | string; + body?: string; + bodySchema?: Object | string; +} ``` -## Documentation +### Output + +```ts +// Field kind describes the type of a field's values +// subjected to the end comparison. +enum ValidationKind { + null // non-comparable data (validation didn't happen) + 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: ValidationKind + values: { // end compared values (coerced, normalized) + actual: any + expected: any + } + errors: FieldError[] + } + } +} + +interface FieldError { + message: string + location?: { // kind-specific additional information + // kind: 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](LICENSE) 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); 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 58ed19de..e7c3b687 100644 --- a/lib/units/validateBody.js +++ b/lib/units/validateBody.js @@ -3,6 +3,7 @@ const mediaTyper = require('media-typer'); const contentTypeUtils = require('content-type'); const { TextDiff, JsonExample, JsonSchema } = require('../validators'); +const isset = require('../utils/isset'); const { isValidField } = require('./isValid'); function isPlainText(mediaType) { @@ -126,16 +127,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]) => { @@ -143,13 +145,13 @@ 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)}'.`; - return [error, null]; + )}' against the expected media type '${mediaTyper.format(expectedType)}'.`; + return [error, null, null]; } - return [null, validator[0]]; + return [null, validator[0], validator[2]]; } /** @@ -157,10 +159,20 @@ function getBodyValidator(realType, expectedType) { * @param {Object} expected * @param {Object} real */ -function validateBody(expected, real) { +function validateBody(expected, actual) { + const values = { + actual: actual.body + }; + + // Prevent assigning { expected: undefined }. + // Also ignore "bodySchema" as the expected value. + if (isset(expected.body)) { + values.expected = expected.body; + } + const errors = []; - const realBodyType = typeof real.body; - const hasEmptyRealBody = real.body === ''; + const realBodyType = typeof actual.body; + const hasEmptyRealBody = actual.body === ''; // Throw when user input for real body is not a string. if (realBodyType !== 'string') { @@ -170,8 +182,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' ); @@ -185,13 +197,15 @@ function validateBody(expected, real) { if (realTypeError) { errors.push({ - message: realTypeError + message: realTypeError, + values }); } if (expectedTypeError) { errors.push({ - message: expectedTypeError + message: expectedTypeError, + values }); } @@ -199,8 +213,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) { @@ -213,11 +227,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 }); } } @@ -226,19 +242,22 @@ function validateBody(expected, real) { const validator = ValidatorClass && new ValidatorClass( - real.body, - usesJsonSchema ? expected.bodySchema : expected.body + usesJsonSchema ? expected.bodySchema : expected.body, + actual.body ); - const rawData = validator && validator.validate(); + + // Calling "validate()" often updates an internal state of a validator. + // That state is later used to output the gavel-compliant results. + // Cannot remove until validators are refactored into simple functions. + // @see https://github.com/apiaryio/gavel.js/issues/150 + 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, + values, errors }; } diff --git a/lib/units/validateHeaders.js b/lib/units/validateHeaders.js index bcfed620..5d5aaef2 100644 --- a/lib/units/validateHeaders.js +++ b/lib/units/validateHeaders.js @@ -14,19 +14,25 @@ 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.expected, values.actual) : 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()); @@ -34,19 +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 { - isValid: isValidField({ errors }), - validator: validator && 'HeadersJsonExample', - realType, - expectedType, - rawData, + valid: isValidField({ errors }), + kind: hasJsonHeaders ? 'json' : 'text', + values, errors }; } diff --git a/lib/units/validateMethod.js b/lib/units/validateMethod.js index ef14f144..ba30ef09 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; +function validateMethod(expected, actual) { + const values = { + expected: expected.method, + actual: actual.method + }; + const valid = values.actual === values.expected; const errors = []; - if (!isValid) { + if (!valid) { errors.push({ - message: `Expected "method" field to equal "${expectedMethod}", but got "${realMethod}".` + message: `Expected method '${values.expected}', but got '${values.actual}'.`, + values }); } return { - isValid, - validator: null, - realType: APIARY_METHOD_TYPE, - expectedType: APIARY_METHOD_TYPE, + valid, + kind: 'text', + values, errors }; } diff --git a/lib/units/validateStatusCode.js b/lib/units/validateStatusCode.js index 5c757514..9d867dd9 100644 --- a/lib/units/validateStatusCode.js +++ b/lib/units/validateStatusCode.js @@ -1,28 +1,27 @@ -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; +function validateStatusCode(expected, actual) { + const values = { + expected: expected.statusCode, + actual: actual.statusCode + }; + const valid = values.actual === values.expected; const errors = []; - if (!isValid) { + if (!valid) { errors.push({ - message: `Status code is '${real.statusCode}' instead of '${ - expected.statusCode - }'` + message: `Expected status code '${values.expected}', but got '${values.actual}'.`, + values }); } return { - isValid, - validator: 'TextDiff', - realType: APIARY_STATUS_CODE_TYPE, - expectedType: APIARY_STATUS_CODE_TYPE, - rawData: '', + valid, + kind: 'text', + values, errors }; } diff --git a/lib/units/validateURI.js b/lib/units/validateURI.js index 7f769c11..8c5a6cb9 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 { uri: expectedURI } = expected; - const { uri: realURI } = real; +const validateURI = (expected, 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, true); - const parsedReal = parseURI(realURI, true); + const parsedExpected = parseURI(values.expected); + const parsedActual = parseURI(values.actual); // 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 '${values.expected}', but got '${values.actual}'.`, + values }); } return { - isValid, - validator: null, - expectedType: APIARY_URI_TYPE, - realType: APIARY_URI_TYPE, + valid, + kind: 'text', + values, errors }; }; diff --git a/lib/validate.js b/lib/validate.js index ba456b72..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.isValid = isValidResult(result); + // Indicates the validity of the actual message + result.valid = isValidResult(fields); + result.fields = fields; return result; } diff --git a/lib/validators/headers-json-example.js b/lib/validators/headers-json-example.js index af4b4074..659d72ad 100644 --- a/lib/validators/headers-json-example.js +++ b/lib/validators/headers-json-example.js @@ -38,9 +38,9 @@ const getSchema = (json) => { }; class HeadersJsonExample extends JsonSchema { - constructor(real, expected) { - if (typeof real !== 'object') { - throw new errors.MalformedDataError('Real is not an Object'); + constructor(expected, actual) { + if (typeof actual !== 'object') { + throw new errors.MalformedDataError('Actual is not an Object'); } if (typeof expected !== 'object') { @@ -48,7 +48,7 @@ class HeadersJsonExample extends JsonSchema { } const preparedExpected = prepareHeaders(expected); - const preparedReal = prepareHeaders(real); + const preparedActual = prepareHeaders(actual); const preparedSchema = getSchema(preparedExpected); if (preparedSchema && preparedSchema.properties) { @@ -60,10 +60,10 @@ class HeadersJsonExample extends JsonSchema { }); } - super(preparedReal, preparedSchema); + super(preparedSchema, preparedActual); this.expected = preparedExpected; - this.real = preparedReal; + this.actual = preparedActual; this.schema = preparedSchema; } diff --git a/lib/validators/json-example.js b/lib/validators/json-example.js index 84b8d323..0e2b2573 100644 --- a/lib/validators/json-example.js +++ b/lib/validators/json-example.js @@ -26,12 +26,12 @@ class JsonExample extends JsonSchema { * @throw {SchemaNotJsonParsableError} when given schema is not a json parsable string or valid json * @throw {NotEnoughDataError} when at least one of expected data and json schema is not given */ - constructor(real, expected) { - if (typeof real !== 'string') { + constructor(expected, actual) { + if (typeof actual !== 'string') { const outError = new errors.MalformedDataError( - 'JsonExample validator: provided real data is not string' + 'JsonExample validator: provided actual data is not string' ); - outError.data = real; + outError.data = actual; throw outError; } @@ -44,7 +44,7 @@ class JsonExample extends JsonSchema { } const schema = getSchema(expected); - super(real, schema); + super(schema, actual); } } diff --git a/lib/validators/json-schema.js b/lib/validators/json-schema.js index c43d716e..14b533a2 100644 --- a/lib/validators/json-schema.js +++ b/lib/validators/json-schema.js @@ -41,17 +41,11 @@ const jsonSchemaOptions = { singleError: false, messages: { minLength: (prop, val, validator) => - `The ${prop} property must be at least ${validator} characters long (currently ${ - val.length - } characters long).`, + `The ${prop} property must be at least ${validator} characters long (currently ${val.length} characters long).`, maxLength: (prop, val, validator) => - `The ${prop} property must not exceed ${validator} characters (currently${ - val.length - } characters long).`, + `The ${prop} property must not exceed ${validator} characters (currently${val.length} characters long).`, length: (prop, val, validator) => - `The ${prop} property must be exactly ${validator} characters long (currently ${ - val.length - } characters long).`, + `The ${prop} property must be exactly ${validator} characters long (currently ${val.length} characters long).`, format: (prop, val, validator) => `The ${prop} property must be ${getArticle( validator[0] @@ -72,13 +66,9 @@ const jsonSchemaOptions = { pattern: (prop, val, validator) => `The ${prop} value (${val}) does not match the ${validator} pattern.`, maxItems: (prop, val, validator) => - `The ${prop} property must not contain more than ${validator} items (currently contains ${ - val.length - } items).`, + `The ${prop} property must not contain more than ${validator} items (currently contains ${val.length} items).`, minItems: (prop, val, validator) => - `The ${prop} property must contain at least ${validator} items (currently contains ${ - val.length - } items).`, + `The ${prop} property must contain at least ${validator} items (currently contains ${val.length} items).`, divisibleBy: (prop, val, validator) => `The ${prop} property is not divisible by ${validator} (current value is ${JSON.stringify( val @@ -90,12 +80,12 @@ const jsonSchemaOptions = { class JsonSchema { /** * Constructs a JsonValidator and validates given data. - * @param {Object | string} data * @param {Object | string} schema + * @param {Object | string} data */ - constructor(data, schema) { - this.data = data; + constructor(schema, data) { this.schema = schema; + this.data = data; if (typeof this.data === 'string') { try { @@ -138,9 +128,7 @@ class JsonSchema { const validationResult = tv4.validateResult(this.schema, metaSchema); if (!validationResult.valid) { throw new errors.JsonSchemaNotValid( - `JSON schema is not valid draft ${this.jsonSchemaVersion}! ${ - validationResult.error.message - } at path "${validationResult.error.dataPath}"` + `JSON schema is not valid draft ${this.jsonSchemaVersion}! ${validationResult.error.message} at path "${validationResult.error.dataPath}"` ); } } @@ -230,23 +218,27 @@ class JsonSchema { const results = Array.from({ length: data.length }, (_, index) => { const item = data[index]; + const { message, property } = item; let pathArray = []; - if (item.property === null) { + if (property === null) { pathArray = []; } else if ( - Array.isArray(item.property) && - item.property.length === 1 && - [null, undefined].includes(item.property[0]) + Array.isArray(property) && + property.length === 1 && + [null, undefined].includes(property[0]) ) { pathArray = []; } else { - pathArray = item.property; + pathArray = property; } return { - pointer: jsonPointer.compile(pathArray), - message: item.message + message, + location: { + pointer: jsonPointer.compile(pathArray), + property + } }; }); @@ -314,9 +306,9 @@ class JsonSchema { const pointer = jsonPointer.compile(pathArray); amandaCompatibleError[index] = { + message: `At '${pointer}' ${error.message}`, property: pathArray, attributeValue: true, - message: `At '${pointer}' ${error.message}`, validatorName: 'error' }; } diff --git a/lib/validators/text-diff.js b/lib/validators/text-diff.js index 0fc73f92..b5923dba 100644 --- a/lib/validators/text-diff.js +++ b/lib/validators/text-diff.js @@ -1,13 +1,12 @@ -const DiffMatchPatch = require('googlediff'); const errors = require('../errors'); class TextDiff { - constructor(real, expected) { - if (typeof real !== 'string') { + constructor(expected, actual) { + if (typeof actual !== 'string') { const outError = new errors.DataNotStringError( - 'String validator real: input data is not string' + 'String validator actual: input data is not string' ); - outError.data = real; + outError.data = actual; throw outError; } @@ -19,51 +18,27 @@ class TextDiff { throw outError; } - this.real = real; this.expected = expected; + this.actual = actual; } validate() { - const sanitizeSurrogatePairs = (data) => { - return data - .replace(/[\uD800-\uDBFF]/g, '') - .replace(/[\uDC00-\uDFFF]/g, ''); - }; - - this.output = null; - const dmp = new DiffMatchPatch(); - - try { - const patch = dmp.patch_make(this.real, this.expected); - this.output = dmp.patch_toText(patch); - return this.output; - } catch (error) { - if (error instanceof URIError) { - const patch = dmp.patch_make( - sanitizeSurrogatePairs(this.real), - sanitizeSurrogatePairs(this.expected) - ); - this.output = dmp.patch_toText(patch); - return this.output; - } - - throw error; - } + this.valid = this.actual === this.expected; + return this.valid; } - evaluateOutputToResults(data) { - if (!data) { - data = this.output; - } - - if (!data) { + evaluateOutputToResults() { + if (this.valid) { return []; } return [ { - // TODO Improve the message to contain real and expected data - message: 'Real and expected data does not match.' + message: 'Actual and expected data do not match.', + values: { + expected: this.expected, + actual: this.actual + } } ]; } diff --git a/package-lock.json b/package-lock.json index 7011e32c..d1f20d4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3483,8 +3483,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3505,14 +3504,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3527,20 +3524,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3657,8 +3651,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3670,7 +3663,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3685,7 +3677,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3693,14 +3684,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3719,7 +3708,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3800,8 +3788,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3813,7 +3800,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3899,8 +3885,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3936,7 +3921,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3956,7 +3940,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4000,14 +3983,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4024,9 +4005,9 @@ "dev": true }, "gavel-spec": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gavel-spec/-/gavel-spec-3.0.2.tgz", - "integrity": "sha512-r8EZvdety8qc80Vf/zJbKCPHYMPBs2Wucg1+qbhTyw20I4doeZZXaB01XQhP8M9qgBnjP7oSJEbNfLputSJ4Dg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/gavel-spec/-/gavel-spec-4.0.0.tgz", + "integrity": "sha512-+8hBF2qyrCpos47y9+PjL0FL5ZwIwvfW7W/RQKLq6X1GJ8C5/KHPLrjOcuRdpwMrz1C7HYPGgYCBuaE3a4iWew==", "dev": true }, "get-assigned-identifiers": { @@ -4174,11 +4155,6 @@ "pinkie-promise": "^2.0.0" } }, - "googlediff": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/googlediff/-/googlediff-0.1.0.tgz", - "integrity": "sha1-mazwXMBiI+tmwpAI2B+bLRjCRT0=" - }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", diff --git a/package.json b/package.json index 06977618..14ab9c39 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,12 @@ "coveralls": "nyc --reporter=text-lcov npm run test:server | coveralls", "ci:lint": "npm run lint", "ci:test": "npm run coveralls && npm run test:browser && npm run test:features", - "ci:release": "semantic-release", - "precommit": "lint-staged" + "ci:release": "semantic-release" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } }, "lint-staged": { "*.js": [ @@ -37,7 +41,6 @@ "content-type": "1.0.4", "curl-trace-parser": "0.0.10", "deep-equal": "1.0.1", - "googlediff": "0.1.0", "http-string-parser": "0.0.6", "json-parse-helpfulerror": "1.0.3", "json-pointer": "0.6.0", @@ -54,7 +57,7 @@ "eslint-config-airbnb-base": "13.2.0", "eslint-config-prettier": "6.0.0", "eslint-plugin-import": "2.18.0", - "gavel-spec": "3.0.2", + "gavel-spec": "4.0.0", "husky": "3.0.0", "lint-staged": "9.0.2", "mocha": "6.1.4", diff --git a/scripts/cucumber.js b/scripts/cucumber.js index 4109a69d..c986f967 100644 --- a/scripts/cucumber.js +++ b/scripts/cucumber.js @@ -1,33 +1,25 @@ +/* eslint-disable import/no-extraneous-dependencies */ const spawn = require('cross-spawn'); const isWindows = process.platform.match(/^win/); -// Removing '@cli' behavior from tests due to +// Excludes Cucumber features marked with the "@cli" tag. +// CLI does not work on Windows: // https://github.com/apiaryio/gavel-spec/issues/24 -const tags = [ - '@javascript', - '~@proposal', - '~@draft', - '~@javascript-pending', - isWindows && '~@cli' -].filter(Boolean); +const tags = [isWindows && '-t ~@cli'].filter(Boolean); -const args = tags.reduce((acc, tag) => { - return acc.concat('-t').concat(tag); -}, []); +const args = [ + ...tags, + '-r', + 'test/cucumber/support/', + '-r', + 'test/cucumber/steps/', + '-f', + 'pretty', + 'node_modules/gavel-spec/features/' +]; -const cucumber = spawn( - 'node_modules/.bin/cucumber-js', - args.concat([ - '-r', - 'test/cucumber/support/', - '-r', - 'test/cucumber/step_definitions/', - '-f', - 'pretty', - 'node_modules/gavel-spec/features/' - ]) -); +const cucumber = spawn('node_modules/.bin/cucumber-js', args); cucumber.stdout.on('data', (data) => process.stdout.write(data)); cucumber.stderr.on('data', (data) => process.stderr.write(data)); diff --git a/test/chai.js b/test/chai.js index 43b6ea6f..afdfca48 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,51 +19,80 @@ 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(target), + JSON.stringify(expectedValue), true ); }); }; createErrorPropertyAssertion('message', 'withMessage'); - createErrorPropertyAssertion('pointer', 'withPointer'); + createErrorPropertyAssertion('location', 'withLocation'); + createErrorPropertyAssertion('values', 'withValues'); + + 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 "${expectedValue}", but got ${kind}. + `, + ` +Expected the following HTTP message field: + +${stringifiedObj} + +to not have "kind" property equal "${expectedValue}". + `, + kind, + expectedValue, + 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 +100,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 +150,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/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 1feec0ec..00000000 --- a/test/cucumber/step_definitions/cli_stepdefs.js +++ /dev/null @@ -1,68 +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 real 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 real 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) !== parseInt(expectedExitStatus)) { - 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) !== parseInt(expectedExitStatus)) { - 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 7eb10a60..00000000 --- a/test/cucumber/step_definitions/validators_steps.js +++ /dev/null @@ -1,235 +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)); - this.booleanResult = result.isValid; - 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(/^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) { - 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(); - }); -}; diff --git a/test/cucumber/steps/cli.js b/test/cucumber/steps/cli.js new file mode 100644 index 00000000..e5453d7d --- /dev/null +++ b/test/cucumber/steps/cli.js @@ -0,0 +1,26 @@ +const { assert } = require('chai'); + +module.exports = function() { + this.Given( + /^(I record (expected|actual) raw HTTP message:)|(a header is missing in actual message:)$/, + function(_1, _2, _3, command) { + this.commands.push(command); + } + ); + + this.When( + 'I validate the message using the following Gavel command:', + async function(command) { + this.commands.push(command); + this.exitCode = await this.executeCommands(this.commands); + } + ); + + this.Then(/^exit status is (\d+)$/, function(expectedExitCode) { + assert.equal( + this.exitCode, + expectedExitCode, + `Expected process to exit with code ${expectedExitCode}, but got ${this.exitCode}.` + ); + }); +}; diff --git a/test/cucumber/steps/fields.js b/test/cucumber/steps/fields.js new file mode 100644 index 00000000..3433e4d4 --- /dev/null +++ b/test/cucumber/steps/fields.js @@ -0,0 +1,15 @@ +const chai = require('chai'); +const jhp = require('json-parse-helpfulerror'); + +chai.config.truncateThreshold = 0; +const { expect } = chai; + +module.exports = function() { + this.Then(/^the result 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..efa804d8 --- /dev/null +++ b/test/cucumber/steps/general.js @@ -0,0 +1,94 @@ +const { expect } = require('chai'); +const jhp = require('json-parse-helpfulerror'); + +module.exports = function() { + this.Given( + /^I expect the following HTTP (message|request|response):$/i, + function(_, expectedMessage) { + this.expected = jhp.parse(expectedMessage); + } + ); + + this.Given(/^the actual HTTP (message|request|response) equals:$/i, function( + _, + actualMessage + ) { + this.actual = jhp.parse(actualMessage); + }); + + // Inline value assertion. + this.Given(/^the actual "([^"]*)" is "([^"]*)"/, function(fieldName, value) { + this.actual[fieldName] = value; + }); + + this.Given(/^I expect "([^"]*)" to be "([^"]*)"$/, function( + fieldName, + expectedValue + ) { + this.expected[fieldName] = expectedValue; + }); + + this.Given(/^I expect "([^"]*)" to equal:$/, function(fieldName, codeBlock) { + // Perform conditional code block parsing (if headers, etc.) + this.expected[fieldName] = this.transformCodeBlock(fieldName, codeBlock); + }); + + this.Given(/^I expect "body" to match the following "([^"]*)":$/, function( + bodyType, + value + ) { + switch (bodyType.toLowerCase()) { + case 'json schema': + this.expected.bodySchema = value; + break; + default: + this.expected.body = value; + break; + } + }); + + // Block value assertion. + this.Given(/^the actual "([^"]*)" equals:$/, function(fieldName, codeBlock) { + // Also perform conditional code parsing + this.actual[fieldName] = this.transformCodeBlock(fieldName, codeBlock); + }); + + // Actions + this.When('Gavel validates the HTTP message', function() { + this.validate(); + }); + + // Vocabulary proxy over the previous action for better scenarios readability. + this.When(/^I call "gavel.validate(([^"]*))"$/, function(_, _command) { + this.validate(); + }); + + // Assertions + this.Then(/^the actual 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 stringifiedActual = JSON.stringify(this.result, null, 2); + + expect(this.result).to.deep.equal( + jhp.parse(expectedResult), + `\ +Expected the following result: + +${stringifiedActual} + +to equal: + +${expectedResult} +` + ); + }); + + this.Then(/^the "(\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..d9916598 100644 --- a/test/cucumber/support/world.js +++ b/test/cucumber/support/world.js @@ -5,137 +5,98 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ /* eslint-disable */ -const gavel = require('../../../lib'); const vm = require('vm'); const util = require('util'); const { assert } = require('chai'); +const { exec } = require('child_process'); +const gavel = require('../../../lib'); const HTTP_LINE_DELIMITER = '\n'; class World { constructor() { - this.codeBuffer = ''; - this.commandBuffer = ''; - - // Data for creation of: - // - // - ExpecterHttpResponse - // - ExpectedHttpRequest - // - ExpectedHttpMessage this.expected = {}; + this.actual = {}; - // Data for creation of: - // - // - HttpResponse - // - HttpRequest - // - HttpMessage - this.real = {}; + // Gavel validation result + this.results = {}; - // Parsed HTTP objects for model valdiation - this.model = {}; + // CLI + this.commands = []; + this.exitCode = null; + } - // Results of validators - this.results = {}; + executeCommands(commands) { + const commandsBuffer = commands.join(';'); + const cmd = + `PATH=$PATH:${process.cwd()}/bin:${process.cwd()}/node_modules/.bin; cd /tmp/gavel-* ;` + + commandsBuffer; - // Validation verdict for the whole HTTP Message - this.booleanResult = false; + return new Promise((resolve) => { + const child = exec(cmd, function(error, stdout, stderr) { + if (error) { + resolve(error.code); + } + }); - // Component relevant to the expectation, e.g. 'body' - this.component = null; - this.componentResults = null; + child.on('exit', function(code) { + resolve(code); + }); + }); + } - this.expectedType = null; - this.realType = null; + validate() { + this.result = gavel.validate(this.expected, this.actual); } - expectBlockEval(block, expectedReturn, callback) { - const realOutput = this.safeEval(block, callback); - - // I'm terribly sorry, but curly braces not asigned to any - // variable in evaled string are interpreted as code block - // not an Object literal, so I'm wrapping expected code output - // with brackets. - // see: http://stackoverflow.com/questions/8949274/javascript-calling-eval-on-an-object-literal-with-functions - - const expectedOutput = this.safeEval(`(${expectedReturn})`, callback); - - const realOutputInspect = util.inspect(realOutput); - const expectedOutputInspect = util.inspect(expectedOutput); - - try { - assert.deepEqual(realOutput, expectedOutput); - } catch (error) { - callback( - new Error( - 'Output of code buffer does not equal. Expected output:\n' + - expectedOutputInspect + - '\nbut got: \n' + - realOutputInspect + - '\n' + - 'Evaled code block:' + - '\n' + - '- - - \n' + - block + - '\n' + - '- - - ' - ) - ); + transformCodeBlock(fieldName, value) { + switch (fieldName) { + case 'headers': + return this.parseHeaders(value); + default: + return value; } - return callback(); } - safeEval(code, callback) { - // I'm terribly sorry, it's no longer possible to manipulate module require/load - // path inside node's process. So I'm prefixing require path by hard - // substitution in code to pretend to 'hit' is packaged module. - // - // further reading on node.js load paths: - // http://nodejs.org/docs/v0.8.23/api/all.html#all_all_together - - const formattedCode = code.replace( - "require('gavel", - "require('../../../lib" - ); + parseHeaders(headersString) { + const lines = headersString.split(HTTP_LINE_DELIMITER); - try { - return eval(formattedCode); - } catch (error) { - return callback( - new Error( - 'Eval failed. Code buffer: \n\n' + - formattedCode + - '\nWith error: ' + - error - ) + 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. +` ); - } - } - validate() { - return gavel.validate(this.expected, this.real); - } + const [_, key, value] = match; - 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(); - } + 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; } parseResponseLine(parsed, firstLine) { - firstLine = firstLine.split(' '); - parsed.statusCode = firstLine[1]; - parsed.statusMessage = firstLine[2]; + const [statusCode] = firstLine.split(' '); + parsed.statusCode = statusCode; } parseHttp(type, string) { @@ -144,7 +105,6 @@ class World { } const parsed = {}; - const lines = string.split(HTTP_LINE_DELIMITER); if (type === 'request') { @@ -157,6 +117,7 @@ class World { const bodyLines = []; const headersLines = []; let bodyEntered = false; + for (let line of Array.from(lines)) { if (line === '') { bodyEntered = true; @@ -177,21 +138,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() ); diff --git a/test/integration/validate.test.js b/test/integration/validate.test.js index a6e2baaf..f39e1f4f 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); @@ -91,30 +74,20 @@ 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'.`); }); }); }); 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 +123,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 +165,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); @@ -222,20 +174,14 @@ 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'.`); }); }); }); 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 +222,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); @@ -304,7 +237,10 @@ describe('validate', () => { it('has pointer to missing "Content-Type"', () => { expect(result.fields.headers) .to.have.errorAtIndex(0) - .withPointer('/content-type'); + .withLocation({ + pointer: '/content-type', + property: ['content-type'] + }); }); it('has explanatory message', () => { @@ -349,11 +285,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', () => { @@ -363,9 +295,16 @@ 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', () => { + expect(result.fields.method) + .to.have.errorAtIndex(0) + .withValues({ + expected: 'POST', + actual: 'PUT' + }); }); }); }); @@ -374,14 +313,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); @@ -390,21 +322,14 @@ 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'.`); }); }); }); 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 +347,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/support/amanda-to-gavel-shared.js b/test/unit/support/amanda-to-gavel-shared.js index 1a233c18..9c76ae01 100644 --- a/test/unit/support/amanda-to-gavel-shared.js +++ b/test/unit/support/amanda-to-gavel-shared.js @@ -45,7 +45,7 @@ exports.shouldBehaveLikeAmandaToGavel = (instance) => { assert.isObject(item); }); - const props = ['message', 'pointer']; + const props = ['message', 'location']; props.forEach((key) => { it('should have "' + key + '"', () => { assert.include(Object.keys(item), key); @@ -55,7 +55,7 @@ exports.shouldBehaveLikeAmandaToGavel = (instance) => { describe('pointer key value', () => { value = null; before(() => { - value = item['pointer']; + value = item.location.pointer; }); it('should be a string', () => { @@ -63,9 +63,8 @@ exports.shouldBehaveLikeAmandaToGavel = (instance) => { }); it('should be a parseable JSON poitner', () => { - parsed = jsonPointer.parse(value); - - assert.isArray(parsed); + const parsedPointer = jsonPointer.parse(value); + assert.isArray(parsedPointer); }); }); }); diff --git a/test/unit/units/validateBody.test.js b/test/unit/units/validateBody.test.js index 846de9b6..480daac9 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', () => { @@ -56,9 +48,18 @@ 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'.` ); }); + + 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', () => { @@ -287,7 +240,7 @@ describe('validateBody', () => { it('with explanatory message', () => { expect(result) .to.have.errorAtIndex(0) - .withMessage('Real and expected data does not match.'); + .withMessage('Actual and expected data do not match.'); }); }); }); @@ -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..9a977444 100644 --- a/test/unit/units/validateBody/getBodyValidator.test.js +++ b/test/unit/units/validateBody/getBodyValidator.test.js @@ -43,24 +43,4 @@ describe('getBodyValidator', () => { }); }); }); - - // 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 }); - // }); - // }); - // }); - // }); }); diff --git a/test/unit/units/validateHeaders.test.js b/test/unit/units/validateHeaders.test.js index 6a4d4c75..4114e4f1 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', () => { @@ -92,7 +68,10 @@ describe('validateHeaders', () => { it('has pointer to header name', () => { expect(result) .to.have.errorAtIndex(index) - .withPointer(`/${headerName}`); + .withLocation({ + pointer: `/${headerName}`, + property: [headerName] + }); }); it('has explanatory message', () => { @@ -122,16 +101,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..d5629846 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', () => { @@ -67,9 +51,16 @@ 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', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: 'POST', + actual: 'GET' + }); }); }); }); @@ -88,16 +79,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', () => { @@ -108,7 +91,16 @@ 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', () => { + 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..d510ed7c 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', () => { @@ -67,7 +51,16 @@ 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', () => { + 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..de706cba 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', () => { @@ -167,9 +127,16 @@ 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', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/dashboard', + actual: '/profile' + }); }); }); }); @@ -189,16 +156,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', () => { @@ -210,9 +169,18 @@ 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'.` ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/account?id=123', + actual: '/account' + }); + }); }); }); @@ -230,16 +198,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', () => { @@ -251,9 +211,18 @@ 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'.` ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/account?name=user', + actual: '/account?nAmE=usEr' + }); + }); }); }); @@ -271,16 +240,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', () => { @@ -292,9 +253,18 @@ 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'.` ); }); + + it('includes values', () => { + expect(result) + .to.have.errorAtIndex(0) + .withValues({ + expected: '/zoo?type=cats&type=dogs', + actual: '/zoo?type=dogs&type=cats' + }); + }); }); }); }); diff --git a/test/unit/validators/headers-json-example-validator-test.js b/test/unit/validators/headers-json-example-validator-test.js index 1319c8a7..7c879552 100644 --- a/test/unit/validators/headers-json-example-validator-test.js +++ b/test/unit/validators/headers-json-example-validator-test.js @@ -16,7 +16,7 @@ describe('HeadersJsonExample', () => { describe('when I provede real data as non obejct', () => { it('should throw an exception', () => { const fn = () => { - headersValidator = new HeadersJsonExample('', { header1: 'value1' }); + headersValidator = new HeadersJsonExample({ header1: 'value1' }, ''); }; assert.throw(fn, 'is not an Object'); }); @@ -25,7 +25,7 @@ describe('HeadersJsonExample', () => { describe('when I provede expected data as non obejct', () => { it('should throw an exception', () => { const fn = () => { - headersValidator = new HeadersJsonExample({ header1: 'value1' }, ''); + headersValidator = new HeadersJsonExample('', { header1: 'value1' }); }; assert.throw(fn, 'is not an Object'); }); @@ -62,8 +62,8 @@ describe('HeadersJsonExample', () => { describe('when provided real and expected headers differ in upper/lower-case state of keys', () => { before(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersMixedCase, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersMixedCase ); }); @@ -78,8 +78,8 @@ describe('HeadersJsonExample', () => { describe('when provided real and expected headers differ in one value (real change) of a key different by upper/lower', () => { before(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersMixedCaseDiffers, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersMixedCaseDiffers ); }); describe('and I run validate()', () => { @@ -93,8 +93,8 @@ describe('HeadersJsonExample', () => { describe('when key is missing in provided headers', () => { beforeEach(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersMissing, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersMissing ); }); describe('and i run validate()', () => { @@ -113,8 +113,8 @@ describe('HeadersJsonExample', () => { describe('when value of content negotiation header in provided headers differs', () => { beforeEach(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersDiffers, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersDiffers ); }); @@ -138,8 +138,8 @@ describe('HeadersJsonExample', () => { describe('when key is added to provided headers', () => { before(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersAdded, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersAdded ); }); @@ -153,7 +153,7 @@ describe('HeadersJsonExample', () => { describe('when real is empty object and expected is proper object', () => { before(() => { - headersValidator = new HeadersJsonExample({}, fixtures.sampleHeaders); + headersValidator = new HeadersJsonExample(fixtures.sampleHeaders, {}); }); describe('and i run validate()', () => { @@ -167,8 +167,8 @@ describe('HeadersJsonExample', () => { describe('when non content negotiation header header values differs', () => { before(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersWithNonContentNegotiationChanged, - fixtures.sampleHeadersNonContentNegotiation + fixtures.sampleHeadersNonContentNegotiation, + fixtures.sampleHeadersWithNonContentNegotiationChanged ); }); @@ -184,8 +184,8 @@ describe('HeadersJsonExample', () => { output = null; before(() => { headersValidator = new HeadersJsonExample( - fixtures.sampleHeadersMissing, - fixtures.sampleHeaders + fixtures.sampleHeaders, + fixtures.sampleHeadersMissing ); output = headersValidator.validate(); }); diff --git a/test/unit/validators/json-example-test.js b/test/unit/validators/json-example-test.js index 748ae47f..16add7e1 100644 --- a/test/unit/validators/json-example-test.js +++ b/test/unit/validators/json-example-test.js @@ -12,10 +12,9 @@ describe('JsonExample', () => { describe('when I provide non string real data', () => { it('should throw exception', () => { const fn = () => { - bodyValidator = new JsonExample( - { malformed: 'malformed ' }, - "{'header1': 'value1'}" - ); + bodyValidator = new JsonExample("{'header1': 'value1'}", { + malformed: 'malformed ' + }); }; assert.throws(fn); }); @@ -25,8 +24,8 @@ describe('JsonExample', () => { it('should not throw exception', () => { fn = () => { bodyValidator = new JsonExample( - '"Number of profiles deleted: com.viacom.auth.infrastructure.DocumentsUpdated@1"', - '{"header1": "value1"}' + '{"header1": "value1"}', + '"Number of profiles deleted: com.viacom.auth.infrastructure.DocumentsUpdated@1"' ); }; assert.doesNotThrow(fn); @@ -37,8 +36,8 @@ describe('JsonExample', () => { it('should not throw exception', () => { const fn = () => { bodyValidator = new JsonExample( - '{"header1": "value1"}', - '"Number of profiles deleted: com.viacom.auth.infrastructure.DocumentsUpdated@1"' + '"Number of profiles deleted: com.viacom.auth.infrastructure.DocumentsUpdated@1"', + '{"header1": "value1"}' ); }; assert.doesNotThrow(fn); @@ -109,8 +108,8 @@ describe('JsonExample', () => { describe('when key is missing in provided real data', () => { before(() => { bodyValidator = new JsonExample( - fixtures.sampleJsonSimpleKeyMissing, - fixtures.sampleJson + fixtures.sampleJson, + fixtures.sampleJsonSimpleKeyMissing ); }); describe('and i run validate()', () => { @@ -122,7 +121,7 @@ describe('JsonExample', () => { describe.skip('when value has different primitive type', () => { before(() => { - bodyValidator = new JsonExample('{"a": "a"}', '{"a": 1}'); + bodyValidator = new JsonExample('{"a": 1}', '{"a": "a"}'); }); describe('and i run validate()', () => { it('PROPOSAL: should return 1 errors', () => { @@ -150,8 +149,8 @@ describe('JsonExample', () => { describe('when key is added to provided data', () => { before(() => { bodyValidator = new JsonExample( - fixtures.sampleJsonComplexKeyAdded, - fixtures.sampleJson + fixtures.sampleJson, + fixtures.sampleJsonComplexKeyAdded ); }); describe('and i run validate()', () => { @@ -180,8 +179,8 @@ describe('JsonExample', () => { describe('when key value is a null', () => { before(() => { bodyValidator = new JsonExample( - '{"a": "a","b": null }', - '{"a":"a", "b": null}' + '{"a":"a", "b": null}', + '{"a": "a","b": null }' ); }); describe('and i run validate()', () => { @@ -195,7 +194,7 @@ describe('JsonExample', () => { describe('when expected and real data are different on root level', () => { describe('when expected is object and real is array', () => { before(() => { - bodyValidator = new JsonExample('[{"a":1}]', '{"a":1}'); + bodyValidator = new JsonExample('{"a":1}', '[{"a":1}]'); }); describe('and i run validate()', () => { it('should not throw exception', () => { @@ -210,7 +209,7 @@ describe('JsonExample', () => { describe('when expected is array and real is object', () => { before(() => { - bodyValidator = new JsonExample('{"a":1}', '[{"a":1}]'); + bodyValidator = new JsonExample('[{"a":1}]', '{"a":1}'); }); describe('and i run validate()', () => { it('should not throw exception', () => { @@ -224,7 +223,7 @@ describe('JsonExample', () => { describe('when expected is primitive and real is object', () => { before(() => { - bodyValidator = new JsonExample('0', '{"a":1}'); + bodyValidator = new JsonExample('{"a":1}', '0'); }); describe('and i run validate()', () => { it('should not throw exception', () => { @@ -238,7 +237,7 @@ describe('JsonExample', () => { describe('when expected array and real is object', () => { before(() => { - bodyValidator = new JsonExample('[0,1,2]', '{"a":1}'); + bodyValidator = new JsonExample('{"a":1}', '[0,1,2]'); }); describe('and i run validate()', () => { it('should not throw exception', () => { @@ -252,7 +251,7 @@ describe('JsonExample', () => { describe('when real is empty object and expected is non-empty object', () => { before(() => { - bodyValidator = new JsonExample('{}', '{"a":1}'); + bodyValidator = new JsonExample('{"a":1}', '{}'); }); describe('and i run validate()', () => { diff --git a/test/unit/validators/json-schema-test.js b/test/unit/validators/json-schema-test.js index 0a130a8e..64d86e4d 100644 --- a/test/unit/validators/json-schema-test.js +++ b/test/unit/validators/json-schema-test.js @@ -13,11 +13,11 @@ describe('JsonSchema', () => { const dataForTypes = { string: { - real: fixtures.sampleJsonComplexKeyMissing, + actual: fixtures.sampleJsonComplexKeyMissing, schema: fixtures.sampleJsonSchemaNonStrict }, object: { - real: JSON.parse(fixtures.sampleJsonComplexKeyMissing), + actual: JSON.parse(fixtures.sampleJsonComplexKeyMissing), schema: JSON.parse(fixtures.sampleJsonSchemaNonStrict) } }; @@ -34,12 +34,12 @@ describe('JsonSchema', () => { let validator = null; beforeEach(() => { - validator = new JsonSchema(data.real, data.schema); + validator = new JsonSchema(data.schema, data.actual); }); it('should not throw an exception', () => { const fn = () => { - new JsonSchema(data.real, data.schema); + new JsonSchema(data.schema, data.actual); }; assert.doesNotThrow(fn); }); @@ -134,11 +134,11 @@ describe('JsonSchema', () => { } ); - describe('when validation performed on real empty object', () => { + describe('when validation performed on actual empty object', () => { it('should return some errors', () => { validator = new JsonSchema( - {}, - JSON.parse(fixtures.sampleJsonSchemaNonStrict) + JSON.parse(fixtures.sampleJsonSchemaNonStrict), + {} ); result = validator.validate(); assert.notEqual(validator.validate().length, 0); @@ -176,7 +176,7 @@ describe('JsonSchema', () => { it('should throw an error for "schema"', () => { const invalidStringifiedSchema = require('../../fixtures/invalid-stringified-schema'); const fn = () => { - new JsonSchema({}, invalidStringifiedSchema); + new JsonSchema(invalidStringifiedSchema, {}); }; assert.throw(fn); }); @@ -192,8 +192,8 @@ describe('JsonSchema', () => { fixtures.sampleJsonBodyTestingAmandaMessages ).length; validator = new JsonSchema( - fixtures.sampleJsonBodyTestingAmandaMessages, - fixtures.sampleJsonSchemaTestingAmandaMessages + fixtures.sampleJsonSchemaTestingAmandaMessages, + fixtures.sampleJsonBodyTestingAmandaMessages ); results = validator.validate(); }); @@ -220,7 +220,7 @@ describe('JsonSchema', () => { before(() => { const invalidSchema = require('../../fixtures/invalid-schema-v3'); fn = () => { - validator = new JsonSchema({}, invalidSchema); + validator = new JsonSchema(invalidSchema, {}); }; }); @@ -242,7 +242,7 @@ describe('JsonSchema', () => { before(() => { const validSchema = require('../../fixtures/valid-schema-v3'); fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); }; }); @@ -263,7 +263,7 @@ describe('JsonSchema', () => { before(() => { const invalidSchema = require('../../fixtures/invalid-schema-v4'); fn = () => { - validator = new JsonSchema({}, invalidSchema); + validator = new JsonSchema(invalidSchema, {}); }; }); @@ -285,7 +285,7 @@ describe('JsonSchema', () => { before(() => { validSchema = require('../../fixtures/valid-schema-v4'); fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); }; }); @@ -306,7 +306,7 @@ describe('JsonSchema', () => { validSchema = require('../../fixtures/valid-schema-v3'); delete validSchema['$schema']; fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); }; }); @@ -326,7 +326,7 @@ describe('JsonSchema', () => { validSchema = require('../../fixtures/valid-schema-v4'); delete validSchema['$schema']; fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); }; }); @@ -346,7 +346,7 @@ describe('JsonSchema', () => { validSchema = require('../../fixtures/invalid-schema-v3-v4'); delete validSchema['$schema']; fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); }; }); @@ -375,7 +375,7 @@ describe('JsonSchema', () => { before(() => { validSchema = require('../../fixtures/valid-schema-v3'); delete validSchema['$schema']; - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); v3 = sinon.stub(validator, 'validateSchemaV3'); v4 = sinon.stub(validator, 'validateSchemaV4'); @@ -404,7 +404,7 @@ describe('JsonSchema', () => { before(() => { const validSchema = require('../../fixtures/valid-schema-v4'); delete validSchema['$schema']; - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); sinon.stub(validator, 'validateSchemaV3'); sinon.stub(validator, 'validateSchemaV4'); @@ -434,9 +434,9 @@ describe('JsonSchema', () => { before(() => { const validSchema = require('../../fixtures/valid-schema-v4-with-refs'); - const real = JSON.parse('{ "foo": "bar" }'); + const actual = JSON.parse('{ "foo": "bar" }'); fn = () => { - validator = new JsonSchema(real, validSchema); + validator = new JsonSchema(validSchema, actual); return validator.validatePrivate(); }; }); @@ -452,9 +452,9 @@ describe('JsonSchema', () => { before(() => { const validSchema = require('../../fixtures/valid-schema-v4-with-refs'); - const real = JSON.parse('{ "foo": 1 }'); + const actual = JSON.parse('{ "foo": 1 }'); fn = () => { - validator = new JsonSchema(real, validSchema); + validator = new JsonSchema(validSchema, actual); return validator.validatePrivate(); }; }); @@ -475,9 +475,9 @@ describe('JsonSchema', () => { before(() => { const invalidSchema = require('../../fixtures/invalid-schema-v4-with-refs'); - const real = JSON.parse('{ "foo": "bar" }'); + const actual = JSON.parse('{ "foo": "bar" }'); fn = () => { - validator = new JsonSchema(real, invalidSchema); + validator = new JsonSchema(invalidSchema, actual); return validator.validatePrivate(); }; }); @@ -500,7 +500,7 @@ describe('JsonSchema', () => { const validSchema = require('../../fixtures/valid-schema-v3'); delete validSchema['$schema']; fn = () => { - validator = new JsonSchema({}, validSchema); + validator = new JsonSchema(validSchema, {}); validator.jsonSchemaVersion = null; validator.validatePrivate(); }; diff --git a/test/unit/validators/text-diff-test.js b/test/unit/validators/text-diff-test.js index b9947726..ca1df7ed 100644 --- a/test/unit/validators/text-diff-test.js +++ b/test/unit/validators/text-diff-test.js @@ -1,166 +1,91 @@ -/* eslint-disable */ -const { assert } = require('chai'); -const DiffMatchPatch = require('googlediff'); - -const fixtures = require('../../fixtures'); +/* eslint-disable no-new */ +const { expect } = require('chai'); const { TextDiff } = require('../../../lib/validators/text-diff'); -const { - ValidationErrors -} = require('../../../lib/validators/validation-errors'); describe('TextDiff', () => { - validator = null; - - describe('when i create new instance of validator with incorrect "data" (first argument)', () => { - validator = null; - - it('should throw exception', () => { + describe('when expected non-string data', () => { + it('should throw an exception', () => { const fn = () => { - validator = new TextDiff(null, ''); + new TextDiff(null, ''); }; - assert.throws(fn); + expect(fn).to.throw(); }); }); - describe('when i create new instance of validator with incorrect "expected" (second argument)', () => { - validator = null; - - it('should throw exception', () => { - fn = () => { - validator = new TextDiff('', null); - }; - assert.throws(fn); - }); - }); - - describe('when i create new instance of validator with "Iñtërnâtiônàlizætiøn☃" string as "data"', () => { - validator = null; - - it('should not throw exception', () => { + describe('when given non-string actual data', () => { + it('should throw an exception', () => { const fn = () => { - validator = new TextDiff('Iñtërnâtiônàlizætiøn☃', ''); + new TextDiff('', null); }; - assert.doesNotThrow(fn); - }); - - describe('when I run validate', () => { - it('should not throw exception', () => { - const fn = () => validator.validate(); - assert.doesNotThrow(fn); - }); + expect(fn).to.throw(); }); }); - describe('when i create new instance of validator with surrogate pair in data', () => { - validator = null; + describe('when expected internationalized string', () => { + const expected = 'Iñtërnâtiônàlizætiøn☃'; - it('should not throw exception', () => { - const fn = () => { - validator = new TextDiff('text1\uD800', '\uD800text1'); - }; - assert.doesNotThrow(fn); + it('should resolve on matching actual string', () => { + const validator = new TextDiff(expected, expected); + expect(validator.validate()).to.be.true; }); - describe('when I run validate', () => { - it('should not throw exception', () => { - const fn = () => validator.validate(); - assert.doesNotThrow(fn); - }); + it('should reject on non-matching actual string', () => { + const validator = new TextDiff(expected, 'Nâtiônàl'); + expect(validator.validate()).to.be.false; }); }); - describe('when i create new instance of validator with correct data', () => { - validator = null; + describe('when expected textual data', () => { + const expected = 'john'; - it('should not throw exception', () => { - const fn = () => { - validator = new TextDiff('text1', 'text1'); - }; - assert.doesNotThrow(fn); + it('should resolve when given matching actual data', () => { + const validator = new TextDiff(expected, 'john'); + expect(validator.validate()).to.be.true; }); - describe('when data are same and I run validate', () => { - validationResult = null; - - before(() => { - validator = new TextDiff('text1', 'text1'); - validationResult = validator.validate(); - }); - - it('should set output property', () => { - assert.isDefined(validator.output); - - it('output should be a string', () => { - assert.isString(validator.output); - }); - - it('output should be empty string', () => { - assert.equal(validator.output, ''); - }); - }); + it('should reject when given non-matching actual data', () => { + const validator = new TextDiff(expected, 'barry'); + expect(validator.validate()).to.be.false; }); + }); - describe('when data differs and I run validate', () => { - validationResult = null; - - before(() => { - validator = new TextDiff('text1', 'text2'); - validationResult = validator.validate(); - }); - - it('output property should be a string', () => { - assert.isString(validator.output); - }); - - it('output property should not be empty string', () => { - assert.notEqual(validator.output, ''); - }); - - it('output property should contain + and -', () => { - assert.include(validator.output, '-'); - assert.include(validator.output, '+'); - }); + describe('when evaluating output to results', () => { + describe('when expected and actual data match', () => { + const validator = new TextDiff('john', 'john'); + validator.validate(); + const result = validator.evaluateOutputToResults(); - it('output property should be persed by googlediff to an array', () => { - dmp = new DiffMatchPatch(); - assert.isArray(dmp.patch_fromText(validator.output)); + it('should return an empty array', () => { + expect(result).to.be.instanceOf(Array); + expect(result).to.have.lengthOf(0); }); }); - }); - describe('.evaluateOutputToResults', () => { - data = null; - results = null; - - describe('empty validation result', () => { - before(() => { - validator = new TextDiff('', ''); - validator.validate(); - results = validator.evaluateOutputToResults(); - }); + describe('when expected and actual data do not match', () => { + const validator = new TextDiff('john', 'barry'); + validator.validate(); + const result = validator.evaluateOutputToResults(); it('should return an array', () => { - assert.isArray(results); + expect(result).to.be.instanceOf(Array); }); - it('should has no results', () => { - assert.equal(results.length, 0); + it('should contain exactly one error', () => { + expect(result).to.have.lengthOf(1); }); - }); - describe('non empty validation result', () => { - before(() => { - validator = new TextDiff('abc', 'cde'); - validator.validate(); - results = validator.evaluateOutputToResults(); + it('error should include the "message"', () => { + expect(result[0]).to.have.property( + 'message', + 'Actual and expected data do not match.' + ); }); - it('should return an array', () => { - assert.isArray(results); - }); - - it('should contain one error', () => { - assert.lengthOf(results, 1); + it('error should contain compared values', () => { + expect(result[0]).to.have.deep.property('values', { + expected: 'john', + actual: 'barry' + }); }); }); });