From b99fecf7278b6d57a26cc51a7feeb00dc268685f Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 10 May 2019 10:59:10 +0200 Subject: [PATCH 01/38] refactor: adds "validateStatusCode" unit --- lib/api/test/unit/validateStatusCode.test.js | 49 ++++++++++++++++++++ lib/api/units/validateStatusCode.js | 30 ++++++++++++ package.json | 3 +- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 lib/api/test/unit/validateStatusCode.test.js create mode 100644 lib/api/units/validateStatusCode.js diff --git a/lib/api/test/unit/validateStatusCode.test.js b/lib/api/test/unit/validateStatusCode.test.js new file mode 100644 index 00000000..f0e2e28e --- /dev/null +++ b/lib/api/test/unit/validateStatusCode.test.js @@ -0,0 +1,49 @@ +const { assert } = require('chai'); +const validateStatusCode = require('../../units/validateStatusCode'); + +describe('validateStatusCode', () => { + describe('given matching status codes', () => { + const res = validateStatusCode(200, 200); + + it('has proper validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); + }); + + it('has proper expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); + }); + + it('has proper real type', () => { + assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); + }); + + it('has no errors', () => { + assert.deepPropertyVal(res, 'results', []); + }); + }); + + describe('given non-matching status codes', () => { + const res = validateStatusCode(200, 400); + + it('has proper validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); + }); + + it('has proper expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); + }); + + it('has proper real type', () => { + assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); + }); + + it('has errors', () => { + assert.deepPropertyVal(res, 'results', [ + { + message: 'Real and expected data does not match.', + severity: 'error' + } + ]); + }); + }); +}); diff --git a/lib/api/units/validateStatusCode.js b/lib/api/units/validateStatusCode.js new file mode 100644 index 00000000..95564564 --- /dev/null +++ b/lib/api/units/validateStatusCode.js @@ -0,0 +1,30 @@ +const { TextDiff } = require('../../validators/text-diff'); + +const APIARY_STATUS_CODE_TYPE = 'text/vnd.apiary.status-code'; + +function normalizeStatusCode(statusCode) { + return String(statusCode).trim(); +} + +/** + * Validates given real and expected status codes. + * @param {number} realStatusCode + * @param {number} expectedStatusCode + */ +function validateStatusCode(realStatusCode, expectedStatusCode) { + const validator = new TextDiff( + normalizeStatusCode(realStatusCode), + normalizeStatusCode(expectedStatusCode) + ); + const rawData = validator.validate(); + + return { + validator: 'TextDiff', + realType: APIARY_STATUS_CODE_TYPE, + expectedType: APIARY_STATUS_CODE_TYPE, + rawData, + results: validator.evaluateOutputToResults() + }; +} + +module.exports = validateStatusCode; diff --git a/package.json b/package.json index 55f1cefa..74a6958b 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,14 @@ "scripts": { "lint": "eslint lib/**/*.js test/**/*.js", "test": "npm run test:server && npm run test:browser && npm run test:features", + "test:new": "mocha \"lib/api/test/**/*.test.js\"", "test:server": "mocha \"test/unit/**/*-test.js\"", "test:server:coverage": "nyc npm run test:server", "test:browser": "mochify \"test/unit/**/*.js\"", "test:features": "node scripts/cucumber.js", "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:test": "npm run coveralls && npm run test:new && npm run test:browser && npm run test:features", "ci:release": "semantic-release", "precommit": "lint-staged" }, From 843dea1357e0acce7cdca90e4f219e7e2001643f Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 10 May 2019 11:46:51 +0200 Subject: [PATCH 02/38] refactor: adds "validateHeaders" unit --- lib/api/test/unit/validateHeaders.test.js | 124 ++++++++++++++++++++++ lib/api/units/validateHeaders.js | 68 ++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 lib/api/test/unit/validateHeaders.test.js create mode 100644 lib/api/units/validateHeaders.js diff --git a/lib/api/test/unit/validateHeaders.test.js b/lib/api/test/unit/validateHeaders.test.js new file mode 100644 index 00000000..cfa42112 --- /dev/null +++ b/lib/api/test/unit/validateHeaders.test.js @@ -0,0 +1,124 @@ +const { assert } = require('chai'); +const validateHeaders = require('../../units/validateHeaders'); + +describe('validateHeaders', () => { + describe('given matching headers', () => { + const res = validateHeaders( + { + 'content-type': 'application/json', + connection: 'keep-alive' + }, + { + 'content-type': 'application/json', + connection: 'keep-alive' + } + ); + + it('has proper validator', () => { + assert.propertyVal(res, 'validator', 'HeadersJsonExample'); + }); + + it('has proper real type', () => { + assert.propertyVal( + res, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has proper expected type', () => { + assert.propertyVal( + res, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no errors', () => { + assert.deepPropertyVal(res, 'results', []); + }); + }); + + describe('given non-matching headers', () => { + const res = validateHeaders( + { + connection: 'keep-alive' + }, + { + 'content-type': 'application/json', + connection: 'keep-alive' + } + ); + + it('has proper validator', () => { + assert.propertyVal(res, 'validator', 'HeadersJsonExample'); + }); + + it('has proper real type', () => { + assert.propertyVal( + res, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has proper expected type', () => { + assert.propertyVal( + res, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + describe('has error', () => { + it('for each missing header', () => { + assert.lengthOf(res.results, 1); + }); + + it('contains pointer to missing header(s)', () => { + assert.propertyVal(res.results[0], 'pointer', '/content-type'); + }); + + it('contains error message', () => { + assert.propertyVal( + res.results[0], + 'message', + `At '/content-type' Missing required property: content-type` + ); + }); + + it('contains proper severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + }); + }); + + describe('given non-json headers', () => { + const res = validateHeaders('foo', 'bar'); + + it('has no validator', () => { + assert.propertyVal(res, 'validator', null); + }); + + it('has no real type', () => { + assert.propertyVal(res, 'realType', null); + }); + + it('has no expected type', () => { + assert.propertyVal(res, 'expectedType', null); + }); + + it('has invalid format error', () => { + assert.lengthOf(res.results, 1); + assert.propertyVal(res.results[0], 'severity', 'error'); + assert.propertyVal( + res.results[0], + 'message', + `No validator found for real data media type +"null" +and expected data media type +"null".` + ); + }); + }); +}); diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js new file mode 100644 index 00000000..07e934d2 --- /dev/null +++ b/lib/api/units/validateHeaders.js @@ -0,0 +1,68 @@ +const { HeadersJsonExample } = require('../../validators/headers-json-example'); + +const APIARY_JSON_HEADER_TYPE = 'application/vnd.apiary.http-headers+json'; + +function normalizeHeaders(headers) { + return ( + headers instanceof Object && + Object.keys(headers).reduce( + (acc, headerName) => + Object.assign({}, acc, { + [headerName.toLowerCase()]: headers[headerName] + }), + {} + ) + ); +} + +function getHeadersType(headers) { + return headers instanceof Object && !Array.isArray(headers) + ? APIARY_JSON_HEADER_TYPE + : null; +} + +/** + * Validates given real and expected headers. + * @param {Object} realHeaders + * @param {Object} expectedHeaders + */ +function validateHeaders(realHeaders, expectedHeaders) { + const real = normalizeHeaders(realHeaders); + const expected = normalizeHeaders(expectedHeaders); + const realType = getHeadersType(real); + const expectedType = getHeadersType(expected); + const results = []; + + const hasJsonHeaders = + realType === APIARY_JSON_HEADER_TYPE && + expectedType === APIARY_JSON_HEADER_TYPE; + + const validator = hasJsonHeaders + ? new HeadersJsonExample(real, expected) + : null; + const rawData = validator && validator.validate(); + + if (validator) { + results.push(...validator.evaluateOutputToResults()); + } else { + results.push({ + message: `\ +No validator found for real data media type +"${JSON.stringify(realType)}" +and expected data media type +"${JSON.stringify(expectedType)}".\ +`, + severity: 'error' + }); + } + + return { + validator: validator && 'HeadersJsonExample', + realType, + expectedType, + rawData, + results + }; +} + +module.exports = validateHeaders; From 788a82465818b98213183f4f4ceb828571b50c54 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 10 May 2019 13:59:21 +0200 Subject: [PATCH 03/38] refactor: adds "validateBody" unit --- lib/api/test/unit/validateBody.test.js | 282 +++++++++++++++++++++++++ lib/api/units/validateBody.js | 233 ++++++++++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 lib/api/test/unit/validateBody.test.js create mode 100644 lib/api/units/validateBody.js diff --git a/lib/api/test/unit/validateBody.test.js b/lib/api/test/unit/validateBody.test.js new file mode 100644 index 00000000..cbecf7ba --- /dev/null +++ b/lib/api/test/unit/validateBody.test.js @@ -0,0 +1,282 @@ +const { assert } = require('chai'); +const validateBody = require('../../units/validateBody'); + +describe('validateBody', () => { + describe('when given unsupported body type', () => { + const scenarios = [ + { name: 'number', value: 5 }, + { name: 'array', value: ['foo', 'bar'] }, + { name: 'object', value: { foo: 'bar' } }, + { name: 'null', value: null }, + { name: 'undefined', value: undefined } + ]; + + scenarios.forEach(({ name, value }) => { + it(`errors when given ${name}`, () => { + assert.throw(() => validateBody({ body: value }, { body: value })); + }); + }); + }); + + describe('when given supported body type', () => { + describe('with explicit Content-Type header', () => { + describe('application/json', () => { + describe('with matching body type', () => { + const res = validateBody( + { + body: '{ "foo": "bar" }', + headers: { 'content-type': 'application/json' } + }, + { body: '{ "foo": "bar" }' } + ); + + it('has application/json real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); + }); + + it('has application/json expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); + }); + + it('has JsonExample validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); + }); + + it('has no errors', () => { + assert.lengthOf(res.results, 0); + }); + }); + + describe('with non-matching body type', () => { + const res = validateBody( + { + body: 'foo', + headers: { 'content-type': 'application/json' } + }, + { body: '{ "foo": "bar" }' } + ); + + it('fallbacks to text/plain real type', () => { + assert.propertyVal(res, 'realType', 'text/plain'); + }); + + it('has application/json expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); + }); + + describe('produces content-type error', () => { + it('has proper severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.match( + res.results[0].message, + /^Real body 'Content-Type' header is 'application\/json' but body is not a parseable JSON:/ + ); + }); + }); + + it('has no validator', () => { + assert.propertyVal(res, 'validator', null); + }); + }); + }); + + describe('application/hal+json', () => { + describe('with matching body type', () => { + const res = validateBody( + { + body: '{ "foo": "bar" }', + headers: { + 'content-type': 'application/hal+json' + } + }, + { + body: '{ "foo": "bar" }' + } + ); + + it('has application/hal+json real type', () => { + assert.propertyVal(res, 'realType', 'application/hal+json'); + }); + + it('has application/json expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); + }); + + it('has JsonExample validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); + }); + + it('has no errors', () => { + assert.lengthOf(res.results, 0); + }); + }); + + describe('with non-matching body type', () => { + const res = validateBody( + { + body: 'text', + headers: { + 'content-type': 'application/hal+json' + } + }, + { + body: 'text' + } + ); + + it('fallbacks to text/plain real type', () => { + assert.propertyVal(res, 'realType', 'text/plain'); + }); + + it('has text/plain expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/plain'); + }); + + describe('produces error', () => { + it('has "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + it('has explanatory message', () => { + assert.match( + res.results[0].message, + /^Real body 'Content-Type' header is 'application\/hal\+json' but body is not a parseable JSON:/ + ); + }); + }); + + it('has no validator', () => { + assert.propertyVal(res, 'validator', null); + }); + }); + }); + }); + + describe('without explicit Content-Type header', () => { + describe('text/plain', () => { + describe('with matching bodies', () => { + const res = validateBody({ body: 'foo' }, { body: 'foo' }); + + it('has text/plain real type', () => { + assert.propertyVal(res, 'realType', 'text/plain'); + }); + + it('has text/plain expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/plain'); + }); + + it('has TextDiff validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); + }); + + it('has no errors', () => { + assert.lengthOf(res.results, 0); + }); + }); + + describe('with non-matching bodies', () => { + const res = validateBody({ body: 'foo ' }, { body: 'bar' }); + + it('has text/plain real type', () => { + assert.propertyVal(res, 'realType', 'text/plain'); + }); + + it('has text/plain expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/plain'); + }); + + it('has TextDiff validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); + }); + + describe('produces validation error', () => { + it('exactly one error', () => { + assert.lengthOf(res.results, 1); + }); + + it('with "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('with explanatory message', () => { + assert.hasAnyKeys(res.results[0], 'message'); + assert.propertyVal( + res.results[0], + 'message', + 'Real and expected data does not match.' + ); + }); + }); + }); + }); + + describe('application/json', () => { + describe('with matching bodies', () => { + const res = validateBody( + { body: '{ "foo": "bar" }' }, + { body: '{ "foo": "bar" }' } + ); + + it('has application/json real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); + }); + + it('has application/json expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); + }); + + it('has JsonExample validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); + }); + + it('has no errors', () => { + assert.lengthOf(res.results, 0); + }); + }); + + describe('with non-matching bodies', () => { + const res = validateBody( + { body: '{ "foo": "bar" }' }, + { body: '{ "bar": null }' } + ); + + it('has application/json real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); + }); + + it('has application/json expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); + }); + + it('has JsonExample validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); + }); + + describe('produces validation errors', () => { + it('exactly one error', () => { + assert.lengthOf(res.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.results[0], + 'message', + `At '/bar' Missing required property: bar` + ); + }); + }); + }); + }); + + describe.skip('application/schema+json', () => { + // ... + }); + }); + }); +}); diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js new file mode 100644 index 00000000..3e8f18c3 --- /dev/null +++ b/lib/api/units/validateBody.js @@ -0,0 +1,233 @@ +const jph = require('json-parse-helpfulerror'); +const mediaTyper = require('media-typer'); +const contentTypeUtils = require('content-type'); + +const { TextDiff } = require('../../../lib/validators/text-diff'); +const { JsonExample } = require('../../../lib/validators/json-example'); +const { JsonSchema } = require('../../../lib/validators/json-schema'); + +function isPlainText(mediaType) { + return mediaType.type === 'text' && mediaType.subtype === 'plain'; +} + +function isJson(mediaType) { + return mediaType.type === 'application' && mediaType.subtype === 'json'; +} + +function isJsonWeak(mediaType) { + return isJson(mediaType) || mediaType.suffix === 'json'; +} + +/** + * Parses a given content-type header into media type. + * @param {string} contentType + * @returns {[Error, MediaType]} + */ +function parseContentType(contentType) { + try { + const { type } = contentTypeUtils.parse(`${contentType}`); + const mediaType = mediaTyper.parse(type); + return [null, mediaType]; + } catch (error) { + return [error, null]; + } +} + +/** + * Determines if a given 'Content-Type' header contains JSON. + * @param {string} contentType + * @returns {boolean} + */ +function isJsonContentType(contentType) { + // No need to explicitly check for "contentType" existence. + // In case given content type is invalid, "parseContentType" + // will return an error, which would be coerced to "false". + const [error, mediaType] = parseContentType(contentType); + + // Silence an error on purporse because contentType + // may contain any kind of rubbish. + return error ? false : isJsonWeak(mediaType); +} + +/** + * Returns a tuple of error and body media type based + * on the given body and normalized headers. + * @param {string} body + * @param {Object} headers + * @returns {[error, bodyType]} + */ +function getBodyType(body, headers) { + const contentType = headers && headers['content-type']; + const hasJsonContentType = isJsonContentType(contentType); + + try { + jph.parse(body); + const [mediaTypeError, bodyMediaType] = parseContentType( + hasJsonContentType ? contentType : 'application/json' + ); + + if (mediaTypeError) { + throw new Error(mediaTypeError); + } + + return [null, bodyMediaType]; + } catch (parsingError) { + const fallbackMediaType = mediaTyper.parse('text/plain'); + const error = hasJsonContentType + ? { + /** + * @TODO The same message for real/expected + * body assertion. Real/expected must be reflected + * in the error message. + */ + message: `\ +Real body 'Content-Type' header is '${contentType}' \ +but body is not a parseable JSON: +${parsingError.message}\ + `, + severity: 'error' + } + : null; + + return [error, fallbackMediaType]; + } +} + +/** + * Returns a tuple of error and schema media type + * based on given body schema. + * @param {string} bodySchema + * @returns {[error, schemaType]} + */ +function getBodySchemaType(bodySchema) { + const jsonSchemaType = mediaTyper.parse('application/schema+json'); + + if (typeof bodySchema !== 'string') { + return [null, jsonSchemaType]; + } + + try { + const parsed = jph.parse(bodySchema); + if (typeof parsed === 'object') { + return [null, jsonSchemaType]; + } + } catch (exception) { + const error = { + message: `Can't validate. Expected body JSON Schema is not a parseable JSON:\n${ + exception.message + }`, + severity: 'error' + }; + + return [error, null]; + } +} + +/** + * Returns a body validator class based on the given + * real and expected body media types. + * @param {MediaType} realType + * @param {MediaType} expectedType + * @returns {Validator} + */ +function getBodyValidator(realType, expectedType) { + const both = (predicate) => (real, expected) => { + return [real, expected].every(predicate); + }; + + const validators = [ + [TextDiff, both(isPlainText)], + // List JsonSchema first, because weak predicate of JsonExample + // would resolve on "application/schema+json" media type too. + [ + JsonSchema, + (real, expected) => { + return ( + isJson(real) && + expected.type === 'application' && + expected.subtype === 'schema' && + expected.suffix === 'json' + ); + } + ], + [JsonExample, both(isJsonWeak)] + ]; + + const validator = validators.find(([_name, predicate]) => { + return predicate(realType, expectedType); + }); + + if (!validator) { + const error = { + message: `Can't validate real media type '${mediaTyper.format( + realType + )}' against expected media type '${mediaTyper.format(expectedType)}'.`, + severity: 'error' + }; + return [error, null]; + } + + return [null, validator[0]]; +} + +/** + * Validates given bodies of transaction elements. + * @param {Object} real + * @param {Object} expected + */ +function validateBody(real, expected) { + const results = []; + const bodyType = typeof real.body; + + if (bodyType !== 'string') { + throw new Error(`Expected HTTP body to be a String, but got: ${bodyType}`); + } + + const [realTypeError, realType] = getBodyType(real.body, real.headers); + const [expectedTypeError, expectedType] = expected.bodySchema + ? getBodySchemaType(expected.bodySchema) + : getBodyType(expected.body, expected.headers); + + if (realTypeError) { + results.push(realTypeError); + } + + if (expectedTypeError) { + results.push(expectedTypeError); + } + + const hasErrors = results.some((result) => + ['error'].includes(result.severity) + ); + + // Skipping body validation in case errors during + // real/expected body type definition. + const [validatorError, ValidatorClass] = hasErrors + ? [null, null] + : getBodyValidator(realType, expectedType); + + if (validatorError) { + results.push(validatorError); + } + + const usesJsonSchema = ValidatorClass && ValidatorClass.name === 'JsonSchema'; + const validator = + ValidatorClass && + new ValidatorClass( + real.body, + usesJsonSchema ? expected.bodySchema : expected.body + ); + const rawData = validator && validator.validate(); + const validatorResults = validator ? validator.evaluateOutputToResults() : []; + results.push(...validatorResults); + + return { + validator: ValidatorClass && ValidatorClass.name, + realType: mediaTyper.format(realType), + expectedType: mediaTyper.format(expectedType), + rawData, + results + }; +} + +module.exports = validateBody; From f9f75c2ace4653bb3f85d92323b9980aafabf241 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 13 May 2019 14:23:02 +0200 Subject: [PATCH 04/38] chore: allow leading underscore for arguments --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 40ff3836..0999a0af 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,6 +6,7 @@ module.exports = { node: true }, rules: { + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 'consistent-return': 'off', 'class-methods-use-this': 'off', 'no-plusplus': 'off', From 492d582552025d74b5e5d29aaaabec76bf11f80e Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 13 May 2019 16:00:39 +0200 Subject: [PATCH 05/38] refactor: uses same signature for validation units --- lib/api/test/unit/validateHeaders.test.js | 31 +++++++++++++++----- lib/api/test/unit/validateStatusCode.test.js | 18 ++++++++++-- lib/api/units/validateHeaders.js | 18 ++++++------ lib/api/units/validateStatusCode.js | 10 +++---- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/lib/api/test/unit/validateHeaders.test.js b/lib/api/test/unit/validateHeaders.test.js index cfa42112..8967338a 100644 --- a/lib/api/test/unit/validateHeaders.test.js +++ b/lib/api/test/unit/validateHeaders.test.js @@ -5,12 +5,16 @@ describe('validateHeaders', () => { describe('given matching headers', () => { const res = validateHeaders( { - 'content-type': 'application/json', - connection: 'keep-alive' + headers: { + 'content-type': 'application/json', + connection: 'keep-alive' + } }, { - 'content-type': 'application/json', - connection: 'keep-alive' + headers: { + 'content-type': 'application/json', + connection: 'keep-alive' + } } ); @@ -42,11 +46,15 @@ describe('validateHeaders', () => { describe('given non-matching headers', () => { const res = validateHeaders( { - connection: 'keep-alive' + headers: { + connection: 'keep-alive' + } }, { - 'content-type': 'application/json', - connection: 'keep-alive' + headers: { + 'content-type': 'application/json', + connection: 'keep-alive' + } } ); @@ -94,7 +102,14 @@ describe('validateHeaders', () => { }); describe('given non-json headers', () => { - const res = validateHeaders('foo', 'bar'); + const res = validateHeaders( + { + headers: 'foo' + }, + { + headers: 'bar' + } + ); it('has no validator', () => { assert.propertyVal(res, 'validator', null); diff --git a/lib/api/test/unit/validateStatusCode.test.js b/lib/api/test/unit/validateStatusCode.test.js index f0e2e28e..8cfd7dd5 100644 --- a/lib/api/test/unit/validateStatusCode.test.js +++ b/lib/api/test/unit/validateStatusCode.test.js @@ -3,7 +3,14 @@ const validateStatusCode = require('../../units/validateStatusCode'); describe('validateStatusCode', () => { describe('given matching status codes', () => { - const res = validateStatusCode(200, 200); + const res = validateStatusCode( + { + statusCode: 200 + }, + { + statusCode: 200 + } + ); it('has proper validator', () => { assert.propertyVal(res, 'validator', 'TextDiff'); @@ -23,7 +30,14 @@ describe('validateStatusCode', () => { }); describe('given non-matching status codes', () => { - const res = validateStatusCode(200, 400); + const res = validateStatusCode( + { + statusCode: 200 + }, + { + statusCode: 400 + } + ); it('has proper validator', () => { assert.propertyVal(res, 'validator', 'TextDiff'); diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js index 07e934d2..bb74e8b3 100644 --- a/lib/api/units/validateHeaders.js +++ b/lib/api/units/validateHeaders.js @@ -22,15 +22,15 @@ function getHeadersType(headers) { } /** - * Validates given real and expected headers. - * @param {Object} realHeaders - * @param {Object} expectedHeaders + * Validates given real and expected transaction elements. + * @param {Object} real + * @param {Object} expected */ -function validateHeaders(realHeaders, expectedHeaders) { - const real = normalizeHeaders(realHeaders); - const expected = normalizeHeaders(expectedHeaders); - const realType = getHeadersType(real); - const expectedType = getHeadersType(expected); +function validateHeaders(real, expected) { + const realHeaders = normalizeHeaders(real.headers); + const expectedHeaders = normalizeHeaders(expected.headers); + const realType = getHeadersType(realHeaders); + const expectedType = getHeadersType(expectedHeaders); const results = []; const hasJsonHeaders = @@ -38,7 +38,7 @@ function validateHeaders(realHeaders, expectedHeaders) { expectedType === APIARY_JSON_HEADER_TYPE; const validator = hasJsonHeaders - ? new HeadersJsonExample(real, expected) + ? new HeadersJsonExample(realHeaders, expectedHeaders) : null; const rawData = validator && validator.validate(); diff --git a/lib/api/units/validateStatusCode.js b/lib/api/units/validateStatusCode.js index 95564564..592cd86a 100644 --- a/lib/api/units/validateStatusCode.js +++ b/lib/api/units/validateStatusCode.js @@ -8,13 +8,13 @@ function normalizeStatusCode(statusCode) { /** * Validates given real and expected status codes. - * @param {number} realStatusCode - * @param {number} expectedStatusCode + * @param {Object} real + * @param {number} expected */ -function validateStatusCode(realStatusCode, expectedStatusCode) { +function validateStatusCode(real, expected) { const validator = new TextDiff( - normalizeStatusCode(realStatusCode), - normalizeStatusCode(expectedStatusCode) + normalizeStatusCode(real.statusCode), + normalizeStatusCode(expected.statusCode) ); const rawData = validator.validate(); From 6182a6671b2c51280f469972ca0f2e3253193693 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 09:33:17 +0200 Subject: [PATCH 06/38] refactor: adds "isJsonSchema" predicate for validateBody --- lib/api/units/validateBody.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 3e8f18c3..fa329097 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -18,6 +18,14 @@ function isJsonWeak(mediaType) { return isJson(mediaType) || mediaType.suffix === 'json'; } +function isJsonSchema(mediaType) { + return ( + mediaType.type === 'application' && + mediaType.subtype === 'schema' && + mediaType.suffix === 'json' + ); +} + /** * Parses a given content-type header into media type. * @param {string} contentType @@ -142,12 +150,7 @@ function getBodyValidator(realType, expectedType) { [ JsonSchema, (real, expected) => { - return ( - isJson(real) && - expected.type === 'application' && - expected.subtype === 'schema' && - expected.suffix === 'json' - ); + return isJson(real) && isJsonSchema(expected); } ], [JsonExample, both(isJsonWeak)] From 3dd9b6da8c7a1cb670c0bb4db208cfd965a5ad10 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 09:41:10 +0200 Subject: [PATCH 07/38] refactor: removes "isJsonWeak" from "validateBody" --- lib/api/units/validateBody.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index fa329097..e2c774a2 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -11,11 +11,10 @@ function isPlainText(mediaType) { } function isJson(mediaType) { - return mediaType.type === 'application' && mediaType.subtype === 'json'; -} - -function isJsonWeak(mediaType) { - return isJson(mediaType) || mediaType.suffix === 'json'; + return ( + (mediaType.type === 'application' && mediaType.subtype === 'json') || + mediaType.suffix === 'json' + ); } function isJsonSchema(mediaType) { @@ -54,7 +53,7 @@ function isJsonContentType(contentType) { // Silence an error on purporse because contentType // may contain any kind of rubbish. - return error ? false : isJsonWeak(mediaType); + return error ? false : isJson(mediaType); } /** @@ -153,7 +152,7 @@ function getBodyValidator(realType, expectedType) { return isJson(real) && isJsonSchema(expected); } ], - [JsonExample, both(isJsonWeak)] + [JsonExample, both(isJson)] ]; const validator = validators.find(([_name, predicate]) => { From 846c45d7e9a08fe5dacfb123d9b3c83bc7a28f6b Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 10:05:40 +0200 Subject: [PATCH 08/38] refactor: accepts "content-type" header directly in "getBodyType" --- lib/api/units/validateBody.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index e2c774a2..47f42107 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -63,8 +63,8 @@ function isJsonContentType(contentType) { * @param {Object} headers * @returns {[error, bodyType]} */ -function getBodyType(body, headers) { - const contentType = headers && headers['content-type']; +function getBodyType(body, contentType) { + // const contentType = headers && headers['content-type']; const hasJsonContentType = isJsonContentType(contentType); try { @@ -185,10 +185,16 @@ function validateBody(real, expected) { throw new Error(`Expected HTTP body to be a String, but got: ${bodyType}`); } - const [realTypeError, realType] = getBodyType(real.body, real.headers); + const [realTypeError, realType] = getBodyType( + real.body, + real.headers && real.headers['content-type'] + ); const [expectedTypeError, expectedType] = expected.bodySchema ? getBodySchemaType(expected.bodySchema) - : getBodyType(expected.body, expected.headers); + : getBodyType( + expected.body, + expected.headers && expected.headers['content-type'] + ); if (realTypeError) { results.push(realTypeError); From e8959de871d34da93474f47c6ee78995815a177d Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 10:21:13 +0200 Subject: [PATCH 09/38] refactor: returns explicit null in "normalizeHeaders" --- lib/api/units/validateHeaders.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js index bb74e8b3..c03722f1 100644 --- a/lib/api/units/validateHeaders.js +++ b/lib/api/units/validateHeaders.js @@ -3,16 +3,15 @@ const { HeadersJsonExample } = require('../../validators/headers-json-example'); const APIARY_JSON_HEADER_TYPE = 'application/vnd.apiary.http-headers+json'; function normalizeHeaders(headers) { - return ( - headers instanceof Object && - Object.keys(headers).reduce( - (acc, headerName) => - Object.assign({}, acc, { - [headerName.toLowerCase()]: headers[headerName] - }), - {} - ) - ); + return headers instanceof Object + ? Object.keys(headers).reduce( + (acc, headerName) => + Object.assign({}, acc, { + [headerName.toLowerCase()]: headers[headerName] + }), + {} + ) + : null; } function getHeadersType(headers) { From 558f7b7dca51a31d1f01debbcb693c8874d4d6d6 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 11:41:19 +0200 Subject: [PATCH 10/38] refactor: delegate error handling to "validateBody" unit --- lib/api/units/validateBody.js | 56 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 47f42107..9f9b0f1e 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -64,7 +64,6 @@ function isJsonContentType(contentType) { * @returns {[error, bodyType]} */ function getBodyType(body, contentType) { - // const contentType = headers && headers['content-type']; const hasJsonContentType = isJsonContentType(contentType); try { @@ -81,19 +80,16 @@ function getBodyType(body, contentType) { } catch (parsingError) { const fallbackMediaType = mediaTyper.parse('text/plain'); const error = hasJsonContentType - ? { - /** - * @TODO The same message for real/expected - * body assertion. Real/expected must be reflected - * in the error message. - */ - message: `\ + ? /** + * @TODO The same message for real/expected + * body assertion. Real/expected must be reflected + * in the error message. + */ + `\ Real body 'Content-Type' header is '${contentType}' \ but body is not a parseable JSON: ${parsingError.message}\ - `, - severity: 'error' - } + ` : null; return [error, fallbackMediaType]; @@ -119,12 +115,9 @@ function getBodySchemaType(bodySchema) { return [null, jsonSchemaType]; } } catch (exception) { - const error = { - message: `Can't validate. Expected body JSON Schema is not a parseable JSON:\n${ - exception.message - }`, - severity: 'error' - }; + const error = `Can't validate. Expected body JSON Schema is not a parseable JSON:\n${ + exception.message + }`; return [error, null]; } @@ -160,12 +153,14 @@ function getBodyValidator(realType, expectedType) { }); if (!validator) { - const error = { - message: `Can't validate real media type '${mediaTyper.format( - realType - )}' against expected media type '${mediaTyper.format(expectedType)}'.`, - severity: 'error' - }; + const error = `Can't validate real media type '${mediaTyper.format( + realType + )}' against expected media type '${mediaTyper.format(expectedType)}'.`; + + // const error = { + // message: + // severity: 'error' + // }; return [error, null]; } @@ -197,11 +192,17 @@ function validateBody(real, expected) { ); if (realTypeError) { - results.push(realTypeError); + results.push({ + message: realTypeError, + severity: 'error' + }); } if (expectedTypeError) { - results.push(expectedTypeError); + results.push({ + message: expectedTypeError, + severity: 'error' + }); } const hasErrors = results.some((result) => @@ -215,7 +216,10 @@ function validateBody(real, expected) { : getBodyValidator(realType, expectedType); if (validatorError) { - results.push(validatorError); + results.push({ + message: validatorError, + severity: 'error' + }); } const usesJsonSchema = ValidatorClass && ValidatorClass.name === 'JsonSchema'; From 9a58c5730f9b35a48425bddb245b3202f4e0562a Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 14 May 2019 13:39:58 +0200 Subject: [PATCH 11/38] refactor: removes content-type parsing error handling from "getBodyType" --- lib/api/units/validateBody.js | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 9f9b0f1e..8470da4b 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -33,10 +33,9 @@ function isJsonSchema(mediaType) { function parseContentType(contentType) { try { const { type } = contentTypeUtils.parse(`${contentType}`); - const mediaType = mediaTyper.parse(type); - return [null, mediaType]; + return mediaTyper.parse(type); } catch (error) { - return [error, null]; + return null; } } @@ -46,14 +45,14 @@ function parseContentType(contentType) { * @returns {boolean} */ function isJsonContentType(contentType) { - // No need to explicitly check for "contentType" existence. - // In case given content type is invalid, "parseContentType" - // will return an error, which would be coerced to "false". - const [error, mediaType] = parseContentType(contentType); - - // Silence an error on purporse because contentType - // may contain any kind of rubbish. - return error ? false : isJson(mediaType); + try { + const mediaType = parseContentType(contentType); + return isJson(mediaType); + } catch (error) { + // Silence an error on purporse because contentType + // may contain any kind of rubbish. + return false; + } } /** @@ -68,14 +67,10 @@ function getBodyType(body, contentType) { try { jph.parse(body); - const [mediaTypeError, bodyMediaType] = parseContentType( + const bodyMediaType = parseContentType( hasJsonContentType ? contentType : 'application/json' ); - if (mediaTypeError) { - throw new Error(mediaTypeError); - } - return [null, bodyMediaType]; } catch (parsingError) { const fallbackMediaType = mediaTyper.parse('text/plain'); @@ -156,11 +151,6 @@ function getBodyValidator(realType, expectedType) { const error = `Can't validate real media type '${mediaTyper.format( realType )}' against expected media type '${mediaTyper.format(expectedType)}'.`; - - // const error = { - // message: - // severity: 'error' - // }; return [error, null]; } From 98967c6235b2c2408aa76ac8ea55fd5cdbc55d5f Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 10:53:24 +0200 Subject: [PATCH 12/38] test: adds granular unit tests for "validateBody" --- .../unit/{ => units}/validateBody.test.js | 2 +- .../validateBody/getBodySchemaType.test.js | 48 +++++++++++ .../units/validateBody/getBodyType.test.js | 78 ++++++++++++++++++ .../validateBody/getBodyValidator.test.js | 80 +++++++++++++++++++ .../unit/units/validateBody/isJson.test.js | 29 +++++++ .../validateBody/isJsonContentType.test.js | 28 +++++++ .../units/validateBody/isJsonSchema.test.js | 25 ++++++ .../unit/{ => units}/validateHeaders.test.js | 2 +- .../{ => units}/validateStatusCode.test.js | 2 +- lib/api/units/validateBody.js | 33 ++++++-- 10 files changed, 319 insertions(+), 8 deletions(-) rename lib/api/test/unit/{ => units}/validateBody.test.js (99%) create mode 100644 lib/api/test/unit/units/validateBody/getBodySchemaType.test.js create mode 100644 lib/api/test/unit/units/validateBody/getBodyType.test.js create mode 100644 lib/api/test/unit/units/validateBody/getBodyValidator.test.js create mode 100644 lib/api/test/unit/units/validateBody/isJson.test.js create mode 100644 lib/api/test/unit/units/validateBody/isJsonContentType.test.js create mode 100644 lib/api/test/unit/units/validateBody/isJsonSchema.test.js rename lib/api/test/unit/{ => units}/validateHeaders.test.js (97%) rename lib/api/test/unit/{ => units}/validateStatusCode.test.js (95%) diff --git a/lib/api/test/unit/validateBody.test.js b/lib/api/test/unit/units/validateBody.test.js similarity index 99% rename from lib/api/test/unit/validateBody.test.js rename to lib/api/test/unit/units/validateBody.test.js index cbecf7ba..0cc6a2fc 100644 --- a/lib/api/test/unit/validateBody.test.js +++ b/lib/api/test/unit/units/validateBody.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateBody = require('../../units/validateBody'); +const { validateBody } = require('../../../units/validateBody'); describe('validateBody', () => { describe('when given unsupported body type', () => { diff --git a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js new file mode 100644 index 00000000..06e5f9e4 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js @@ -0,0 +1,48 @@ +const { assert } = require('chai'); +const mediaTyper = require('media-typer'); +const { getBodySchemaType } = require('../../../../units/validateBody'); + +describe('getBodySchemaType', () => { + describe('when given non-string schema', () => { + // ... + }); + + describe('when given json schema', () => { + describe('that contains object', () => { + const res = getBodySchemaType('{ "foo": "bar" }'); + + it('returns no errors', () => { + assert.isNull(res[0]); + }); + + it('returns "application/schema+json" media type', () => { + assert.deepEqual(res[1], mediaTyper.parse('application/schema+json')); + }); + }); + + describe('that contains array', () => { + const res = getBodySchemaType('[1, 2, 3]'); + + it('returns no errors', () => { + assert.isNull(res[0]); + }); + + it('returns "application/schema+json" media type', () => { + assert.deepEqual(res[1], mediaTyper.parse('application/schema+json')); + }); + }); + }); + + describe('when given non-json schema', () => { + const res = getBodySchemaType('foo'); + + it('returns parsing error', () => { + assert.match( + res[0], + /^Can't validate. Expected body JSON Schema is not a parseable JSON:/ + ); + }); + + it('returns no media type', () => [assert.isNull(res[1])]); + }); +}); diff --git a/lib/api/test/unit/units/validateBody/getBodyType.test.js b/lib/api/test/unit/units/validateBody/getBodyType.test.js new file mode 100644 index 00000000..1301d2d8 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/getBodyType.test.js @@ -0,0 +1,78 @@ +const { assert } = require('chai'); +const mediaTyper = require('media-typer'); +const { getBodyType } = require('../../../../units/validateBody'); + +const jsonTypes = ['application/json', 'application/schema+json']; +const nonJsonTypes = ['text/plain']; + +describe('getBodyType', () => { + describe('when given json-like content type', () => { + jsonTypes.forEach((jsonType) => { + describe(`when given "${jsonType}" content type`, () => { + describe('and parsable json body', () => { + const res = getBodyType('{ "foo": "bar" }', jsonType); + + it('returns no errors', () => { + assert.isNull(res[0]); + }); + + it(`returns "${jsonType}" media type`, () => { + assert.deepEqual(res[1], mediaTyper.parse(jsonType)); + }); + }); + + describe('and non-parsable json body', () => { + const res = getBodyType('abc', jsonType); + + it('returns parsing error', () => { + assert.match( + res[0], + new RegExp( + /* eslint-disable no-useless-escape */ + `^Real body 'Content-Type' header is \'${jsonType.replace( + /(\/|\+)/g, + '\\$1' + )}\' but body is not a parseable JSON:` + /* eslint-enable no-useless-escape */ + ) + ); + }); + + it('uses "text/plain" as fallback media type', () => { + assert.deepEqual(res[1], mediaTyper.parse('text/plain')); + }); + }); + }); + }); + }); + + describe('when given non-json content type', () => { + nonJsonTypes.forEach((nonJsonType) => { + describe(`when given "${nonJsonType}" content type`, () => { + describe('and parsable json body', () => { + const res = getBodyType('{ "foo": "bar" }', nonJsonType); + + it('returns no errors', () => { + assert.isNull(res[0]); + }); + + it('coerces to "application/json" media type', () => { + assert.deepEqual(res[1], mediaTyper.parse('application/json')); + }); + }); + + describe('and a non-json body', () => { + const res = getBodyType('abc', nonJsonType); + + it('returns no errors', () => { + assert.isNull(res[0]); + }); + + it(`returns "${nonJsonType}" media type`, () => { + assert.deepEqual(res[1], mediaTyper.parse(nonJsonType)); + }); + }); + }); + }); + }); +}); diff --git a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js new file mode 100644 index 00000000..387d5ee7 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js @@ -0,0 +1,80 @@ +const { assert } = require('chai'); +const mediaTyper = require('media-typer'); +const { getBodyValidator } = require('../../../../units/validateBody'); + +const getMediaTypes = (real, expected) => { + return [real, expected].map(mediaTyper.parse); +}; + +describe('getBodyValidator', () => { + const knownContentTypes = [ + { + contentTypes: ['application/json', 'application/json'], + expectedValidator: 'JsonExample' + }, + { + contentTypes: ['application/json', 'application/schema+json'], + expectedValidator: 'JsonSchema' + }, + { + contentTypes: ['text/plain', 'text/plain'], + expectedValidator: 'TextDiff' + } + ]; + + const unknownContentTypes = [ + { + contentTypes: ['application/xml', 'application/xml'] + }, + { + contentTypes: ['text/html', 'application/xml'] + } + ]; + + describe('when given known media type', () => { + knownContentTypes.forEach(({ contentTypes, expectedValidator }) => { + const [realContentType, expectedContentType] = contentTypes; + const [real, expected] = getMediaTypes( + realContentType, + expectedContentType + ); + + describe(`${realContentType} + ${expectedContentType}`, () => { + const [error, validator] = getBodyValidator(real, expected); + + it('returns no error', () => { + assert.isNull(error); + }); + + it(`returns "${expectedValidator}" validator`, () => { + assert.equal(validator.name, expectedValidator); + }); + }); + }); + }); + + describe('when given unknown media type', () => { + unknownContentTypes.forEach(({ contentTypes }) => { + const [realContentType, expectedContentType] = contentTypes; + const [real, expected] = getMediaTypes( + realContentType, + expectedContentType + ); + + describe(`${realContentType} + ${expectedContentType}`, () => { + const [error, validator] = getBodyValidator(real, expected); + + it('has explanatory error', () => { + assert.equal( + error, + `Can't validate real media type '${realContentType}' against expected media type '${expectedContentType}'.` + ); + }); + + it('returns null validator', () => { + assert.isNull(validator); + }); + }); + }); + }); +}); diff --git a/lib/api/test/unit/units/validateBody/isJson.test.js b/lib/api/test/unit/units/validateBody/isJson.test.js new file mode 100644 index 00000000..38519801 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/isJson.test.js @@ -0,0 +1,29 @@ +const { assert } = require('chai'); +const mediaTyper = require('media-typer'); +const { isJson } = require('../../../../units/validateBody'); + +describe('isJson', () => { + describe('returns true', () => { + it('when given valid json media type', () => { + assert.isTrue(isJson(mediaTyper.parse('application/json'))); + }); + + it('when given json-compatible media type', () => { + assert.isTrue(isJson(mediaTyper.parse('application/schema+json'))); + }); + }); + + describe('returns false', () => { + it('when given non-json media type', () => { + assert.isFalse(isJson(mediaTyper.parse('text/plain'))); + }); + + it('when given rubbish', () => { + assert.isFalse(isJson('abc')); + }); + + it('when given null', () => { + assert.isFalse(isJson(null)); + }); + }); +}); diff --git a/lib/api/test/unit/units/validateBody/isJsonContentType.test.js b/lib/api/test/unit/units/validateBody/isJsonContentType.test.js new file mode 100644 index 00000000..10d7f0f5 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/isJsonContentType.test.js @@ -0,0 +1,28 @@ +const { assert } = require('chai'); +const { isJsonContentType } = require('../../../../units/validateBody'); + +describe('isJsonContentType', () => { + describe('returns true', () => { + const jsonTypes = ['application/json', 'application/schema+json']; + + jsonTypes.forEach((contentType) => { + it(`when given ${contentType}`, () => { + assert.isTrue(isJsonContentType(contentType)); + }); + }); + }); + + describe('returns false', () => { + const nonJsonTypes = ['application/xml', 'text/plain']; + + nonJsonTypes.forEach((contentType) => { + it(`when given ${contentType}`, () => { + assert.isFalse(isJsonContentType(contentType)); + }); + }); + + it('when given rubbish', () => { + assert.isFalse(isJsonContentType('foo')); + }); + }); +}); diff --git a/lib/api/test/unit/units/validateBody/isJsonSchema.test.js b/lib/api/test/unit/units/validateBody/isJsonSchema.test.js new file mode 100644 index 00000000..a2a4bb15 --- /dev/null +++ b/lib/api/test/unit/units/validateBody/isJsonSchema.test.js @@ -0,0 +1,25 @@ +const { assert } = require('chai'); +const mediaTyper = require('media-typer'); +const { isJsonSchema } = require('../../../../units/validateBody'); + +describe('isJsonSchema', () => { + describe('returns true', () => { + it('when given a json schema media type', () => { + assert.isTrue(isJsonSchema(mediaTyper.parse('application/schema+json'))); + }); + }); + + describe('returns false', () => { + it('when given json media type', () => { + assert.isFalse(isJsonSchema(mediaTyper.parse('application/json'))); + }); + + it('when given rubbish', () => { + assert.isFalse(isJsonSchema('abc')); + }); + + it('when given null', () => { + assert.isFalse(isJsonSchema(null)); + }); + }); +}); diff --git a/lib/api/test/unit/validateHeaders.test.js b/lib/api/test/unit/units/validateHeaders.test.js similarity index 97% rename from lib/api/test/unit/validateHeaders.test.js rename to lib/api/test/unit/units/validateHeaders.test.js index 8967338a..ab5b4b63 100644 --- a/lib/api/test/unit/validateHeaders.test.js +++ b/lib/api/test/unit/units/validateHeaders.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateHeaders = require('../../units/validateHeaders'); +const validateHeaders = require('../../../units/validateHeaders'); describe('validateHeaders', () => { describe('given matching headers', () => { diff --git a/lib/api/test/unit/validateStatusCode.test.js b/lib/api/test/unit/units/validateStatusCode.test.js similarity index 95% rename from lib/api/test/unit/validateStatusCode.test.js rename to lib/api/test/unit/units/validateStatusCode.test.js index 8cfd7dd5..bc0b665e 100644 --- a/lib/api/test/unit/validateStatusCode.test.js +++ b/lib/api/test/unit/units/validateStatusCode.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateStatusCode = require('../../units/validateStatusCode'); +const validateStatusCode = require('../../../units/validateStatusCode'); describe('validateStatusCode', () => { describe('given matching status codes', () => { diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 8470da4b..6f478445 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -11,6 +11,10 @@ function isPlainText(mediaType) { } function isJson(mediaType) { + if (!mediaType) { + return false; + } + return ( (mediaType.type === 'application' && mediaType.subtype === 'json') || mediaType.suffix === 'json' @@ -18,6 +22,10 @@ function isJson(mediaType) { } function isJsonSchema(mediaType) { + if (!mediaType) { + return false; + } + return ( mediaType.type === 'application' && mediaType.subtype === 'schema' && @@ -105,10 +113,8 @@ function getBodySchemaType(bodySchema) { } try { - const parsed = jph.parse(bodySchema); - if (typeof parsed === 'object') { - return [null, jsonSchemaType]; - } + jph.parse(bodySchema); + return [null, jsonSchemaType]; } catch (exception) { const error = `Can't validate. Expected body JSON Schema is not a parseable JSON:\n${ exception.message @@ -151,6 +157,13 @@ function getBodyValidator(realType, expectedType) { const error = `Can't validate real media type '${mediaTyper.format( realType )}' against expected media type '${mediaTyper.format(expectedType)}'.`; + + /** + * Shouldn't we fallback to "TextDiff" validator on unknown content types? + * Unless content type is invalid, it can be at least text diff'ed. + * Considering that the input of this function is an already parsed content type, + * it's impossible for it to be invalid. + */ return [error, null]; } @@ -232,4 +245,14 @@ function validateBody(real, expected) { }; } -module.exports = validateBody; +module.exports = { + validateBody, + + isJson, + isJsonSchema, + isJsonContentType, + parseContentType, + getBodyType, + getBodySchemaType, + getBodyValidator +}; From 79448a3d07695dff027acf8255eb98b182d4f397 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 12:04:12 +0200 Subject: [PATCH 13/38] refactor: adds "evolve" utility --- lib/api/test/unit/utils/evolve.test.js | 74 ++++++++++++++++++++++++++ lib/api/utils/evolve.js | 44 +++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 lib/api/test/unit/utils/evolve.test.js create mode 100644 lib/api/utils/evolve.js diff --git a/lib/api/test/unit/utils/evolve.test.js b/lib/api/test/unit/utils/evolve.test.js new file mode 100644 index 00000000..77398a7f --- /dev/null +++ b/lib/api/test/unit/utils/evolve.test.js @@ -0,0 +1,74 @@ +const { assert } = require('chai'); +const evolve = require('../../../utils/evolve'); + +const multiply = (a) => (n) => n * a; +const unexpectedTypes = [ + ['number', 5], + ['string', 'foo'], + ['function', () => null], + ['null', null], + ['undefined', undefined] +]; + +describe('evolve', () => { + describe('evolves a given object', () => { + const res = evolve({ + a: multiply(2), + c: () => null + })({ + a: 2, + b: 'foo' + }); + + it('evolves matching properties', () => { + assert.propertyVal(res, 'a', 4); + }); + + it('bypasses properties not in schema', () => { + assert.propertyVal(res, 'b', 'foo'); + }); + + it('ignores properties not in data', () => { + assert.notProperty(res, 'c'); + }); + }); + + describe('evolves a given array', () => { + const res = evolve({ + 0: multiply(2), + 1: multiply(3), + 3: multiply(4) + })([1, 2, 3]); + + it('evolves matching keys', () => { + assert.propertyVal(res, 0, 2); + assert.propertyVal(res, 1, 6); + }); + + it('bypasses keys not in schema', () => { + assert.propertyVal(res, 2, 3); + }); + + it('ignores properties not in data', () => { + assert.notProperty(res, 3); + }); + }); + + describe('throws when given unexpected schema', () => { + unexpectedTypes + .concat([['array', [1, 2]]]) + .forEach(([typeName, dataType]) => { + it(`when given ${typeName}`, () => { + assert.throw(() => evolve(dataType)({})); + }); + }); + }); + + describe('throws when given unexpected data', () => { + unexpectedTypes.forEach(([typeName, dataType]) => { + it(`when given ${typeName}`, () => { + assert.throw(() => evolve({ a: () => null })(dataType)); + }); + }); + }); +}); diff --git a/lib/api/utils/evolve.js b/lib/api/utils/evolve.js new file mode 100644 index 00000000..cd2bdb6e --- /dev/null +++ b/lib/api/utils/evolve.js @@ -0,0 +1,44 @@ +/** + * Applies a given evolution schema to the given data Object. + * Properties not present in schema are bypassed. + * Properties not present in data are ignored. + * @param {Object} schema + * @param {Object|Array} data + * @returns {Object} + */ +const evolve = (schema) => (data) => { + const dataType = typeof data; + const schemaType = typeof schema; + const isArray = Array.isArray(data); + const result = isArray ? [] : {}; + + if (!(schema !== null && schemaType === 'object' && !Array.isArray(schema))) { + throw new Error( + `Failed to evolve: expected transformations schema to be an object, but got: ${schemaType}` + ); + } + + if (dataType !== 'object') { + throw new Error( + `Failed to evolve: expected data to be an instance of array or object, but got: ${dataType}` + ); + } + + return Object.keys(data).reduce((acc, key) => { + const value = data[key]; + const transform = schema[key]; + const transformType = typeof transform; + /* eslint-disable no-nested-ternary */ + const nextValue = + transformType === 'function' + ? transform(value) + : transform && transformType === 'object' + ? evolve(transform)(value) + : value; + /* eslint-enable no-nested-ternary */ + + return isArray ? acc.concat(nextValue) : { ...acc, [key]: nextValue }; + }, result); +}; + +module.exports = evolve; From 350d8cafe717b2fd2f561d4ad163b25ebcdc7482 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 12:19:02 +0200 Subject: [PATCH 14/38] fix: adds "normalizeHeaders" --- .../units/normalize/normalizeHeaders.test.js | 43 +++++++++++++++++++ lib/api/units/normalize/index.js | 8 ++++ lib/api/units/normalize/normalizeHeaders.js | 35 +++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 lib/api/test/unit/units/normalize/normalizeHeaders.test.js create mode 100644 lib/api/units/normalize/index.js create mode 100644 lib/api/units/normalize/normalizeHeaders.js diff --git a/lib/api/test/unit/units/normalize/normalizeHeaders.test.js b/lib/api/test/unit/units/normalize/normalizeHeaders.test.js new file mode 100644 index 00000000..bad150a0 --- /dev/null +++ b/lib/api/test/unit/units/normalize/normalizeHeaders.test.js @@ -0,0 +1,43 @@ +const { assert } = require('chai'); +const normalizeHeaders = require('../../../../units/normalize/normalizeHeaders'); + +describe('normalizeHeaders', () => { + describe('when given nothing', () => { + const headers = normalizeHeaders(undefined); + + it('coerces to empty object', () => { + assert.deepEqual(headers, {}); + }); + }); + + describe('when given headers object', () => { + const headers = { + 'Accept-Language': 'en-US, en', + 'Content-Type': 'application/json', + 'Content-Length': 128 + }; + const normalizedHeaders = normalizeHeaders(headers); + + it('lowercases the keys', () => { + const expectedKeys = Object.keys(headers).map((key) => key.toLowerCase()); + assert.hasAllKeys(normalizedHeaders, expectedKeys); + }); + + it('lowercases the values', () => { + const expectedValues = Object.values(headers).map((value) => + typeof value === 'string' ? value.toLowerCase() : value + ); + assert.deepEqual(Object.values(normalizedHeaders), expectedValues); + }); + }); + + describe('when given non-object headers', () => { + const unsupportedTypes = [['string', 'foo'], ['number', 2]]; + + unsupportedTypes.forEach(([typeName, dataType]) => { + it(`when given ${typeName}`, () => { + assert.throw(() => normalizeHeaders(dataType)); + }); + }); + }); +}); diff --git a/lib/api/units/normalize/index.js b/lib/api/units/normalize/index.js new file mode 100644 index 00000000..cd84754c --- /dev/null +++ b/lib/api/units/normalize/index.js @@ -0,0 +1,8 @@ +const evolve = require('../../utils/evolve'); +const normalizeHeaders = require('./normalizeHeaders'); + +const normalize = evolve({ + headers: normalizeHeaders +}); + +module.exports = normalize; diff --git a/lib/api/units/normalize/normalizeHeaders.js b/lib/api/units/normalize/normalizeHeaders.js new file mode 100644 index 00000000..70a1e0c9 --- /dev/null +++ b/lib/api/units/normalize/normalizeHeaders.js @@ -0,0 +1,35 @@ +const normalizeStringValue = (value) => { + return value.toLowerCase().trim(); +}; + +/** + * Normalizes the given headers. + * @param {Object} headers + * @returns {Object} + */ +const normalizeHeaders = (headers) => { + if (!headers) { + return {}; + } + + const headersType = typeof headers; + + if (headersType === null || headersType !== 'object') { + throw new Error( + `Can't validate: expected "headers" to be an Object, but got: ${headersType}.` + ); + } + + return Object.keys(headers).reduce( + (acc, name) => ({ + ...acc, + [name.toLowerCase()]: + typeof headers[name] === 'string' + ? normalizeStringValue(headers[name]) + : headers[name] + }), + {} + ); +}; + +module.exports = normalizeHeaders; From f2c291433f35c0aa6e37ef97d470eb1929212453 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 13:50:53 +0200 Subject: [PATCH 15/38] refactor: removes missing validator handling in "getBodyValidator" --- .../validateBody/getBodyValidator.test.js | 64 +++++-------------- lib/api/units/validateBody.js | 14 ---- 2 files changed, 15 insertions(+), 63 deletions(-) diff --git a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js index 387d5ee7..9e0bfa57 100644 --- a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js +++ b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js @@ -7,31 +7,22 @@ const getMediaTypes = (real, expected) => { }; describe('getBodyValidator', () => { - const knownContentTypes = [ - { - contentTypes: ['application/json', 'application/json'], - expectedValidator: 'JsonExample' - }, - { - contentTypes: ['application/json', 'application/schema+json'], - expectedValidator: 'JsonSchema' - }, - { - contentTypes: ['text/plain', 'text/plain'], - expectedValidator: 'TextDiff' - } - ]; - - const unknownContentTypes = [ - { - contentTypes: ['application/xml', 'application/xml'] - }, - { - contentTypes: ['text/html', 'application/xml'] - } - ]; - describe('when given known media type', () => { + const knownContentTypes = [ + { + contentTypes: ['application/json', 'application/json'], + expectedValidator: 'JsonExample' + }, + { + contentTypes: ['application/json', 'application/schema+json'], + expectedValidator: 'JsonSchema' + }, + { + contentTypes: ['text/plain', 'text/plain'], + expectedValidator: 'TextDiff' + } + ]; + knownContentTypes.forEach(({ contentTypes, expectedValidator }) => { const [realContentType, expectedContentType] = contentTypes; const [real, expected] = getMediaTypes( @@ -52,29 +43,4 @@ describe('getBodyValidator', () => { }); }); }); - - describe('when given unknown media type', () => { - unknownContentTypes.forEach(({ contentTypes }) => { - const [realContentType, expectedContentType] = contentTypes; - const [real, expected] = getMediaTypes( - realContentType, - expectedContentType - ); - - describe(`${realContentType} + ${expectedContentType}`, () => { - const [error, validator] = getBodyValidator(real, expected); - - it('has explanatory error', () => { - assert.equal( - error, - `Can't validate real media type '${realContentType}' against expected media type '${expectedContentType}'.` - ); - }); - - it('returns null validator', () => { - assert.isNull(validator); - }); - }); - }); - }); }); diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 6f478445..1bbcb38b 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -153,20 +153,6 @@ function getBodyValidator(realType, expectedType) { return predicate(realType, expectedType); }); - if (!validator) { - const error = `Can't validate real media type '${mediaTyper.format( - realType - )}' against expected media type '${mediaTyper.format(expectedType)}'.`; - - /** - * Shouldn't we fallback to "TextDiff" validator on unknown content types? - * Unless content type is invalid, it can be at least text diff'ed. - * Considering that the input of this function is an already parsed content type, - * it's impossible for it to be invalid. - */ - return [error, null]; - } - return [null, validator[0]]; } From dd832ae68c6e9dcb585ff8e2e6a7ada6a404a4cb Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 13:51:56 +0200 Subject: [PATCH 16/38] refactor: removes extra escaping for media type in "validateHeaders" --- lib/api/units/validateHeaders.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js index c03722f1..8d9bd439 100644 --- a/lib/api/units/validateHeaders.js +++ b/lib/api/units/validateHeaders.js @@ -47,9 +47,9 @@ function validateHeaders(real, expected) { results.push({ message: `\ No validator found for real data media type -"${JSON.stringify(realType)}" +"${realType}" and expected data media type -"${JSON.stringify(expectedType)}".\ +"${expectedType}".\ `, severity: 'error' }); From b7b44520712dbdc2ab4e82462642b53cc55ccf85 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 13:52:44 +0200 Subject: [PATCH 17/38] refactor: adds "validateRequest" --- .../test/integration/validateRequest.test.js | 168 ++++++++++++++++++ lib/api/validateRequest.js | 11 ++ 2 files changed, 179 insertions(+) create mode 100644 lib/api/test/integration/validateRequest.test.js create mode 100644 lib/api/validateRequest.js diff --git a/lib/api/test/integration/validateRequest.test.js b/lib/api/test/integration/validateRequest.test.js new file mode 100644 index 00000000..0fe83e77 --- /dev/null +++ b/lib/api/test/integration/validateRequest.test.js @@ -0,0 +1,168 @@ +const { assert } = require('chai'); +const validateRequest = require('../../validateRequest'); + +describe('validateRequest', () => { + describe('with matching requests', () => { + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: '{ "foo": "bar" }' + }; + const res = validateRequest(request, request); + + it('returns validation result object', () => { + assert.isObject(res); + }); + + it('contains all validatable keys', () => { + assert.hasAllKeys(res, ['headers', 'body']); + }); + + describe('headers', () => { + it('has "HeadersJsonExample" validator', () => { + assert.propertyVal(res.headers, 'validator', 'HeadersJsonExample'); + }); + + it('has "application/vnd.apiary.http-headers+json" real headers type', () => { + assert.propertyVal( + res.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has "application/vnd.apiary.http-headers+json" expected headers type', () => { + assert.propertyVal( + res.headers, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no errors', () => { + assert.lengthOf(res.headers.results, 0); + }); + }); + + describe('body', () => { + it('has "JsonExample" validator', () => { + assert.propertyVal(res.body, 'validator', 'JsonExample'); + }); + + it('has "application/json" real body type', () => { + assert.propertyVal(res.body, 'realType', 'application/json'); + }); + + it('has "application/json" expected body type', () => { + assert.propertyVal(res.body, 'expectedType', 'application/json'); + }); + + it('has no errors', () => { + assert.lengthOf(res.body.results, 0); + }); + }); + }); + + describe('with non-matching requests', () => { + const res = validateRequest( + { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: '{ "foo": "bar" }' + }, + { + method: 'PUT', + headers: null, + body: '2' + } + ); + + it('returns validation result object', () => { + assert.isObject(res); + }); + + it('contains all validatable keys', () => { + assert.hasAllKeys(res, ['headers', 'body']); + }); + + describe('method', () => { + it.skip('compares methods'); + }); + + describe('headers', () => { + it('has no validator set', () => { + assert.propertyVal(res.headers, 'validator', null); + }); + + it('has "application/vnd.apiary.http-headers+json" as real headers type', () => { + assert.propertyVal( + res.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no expected headers type', () => { + assert.propertyVal(res.headers, 'expectedType', null); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(res.headers.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.headers.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.headers.results[0], + 'message', + `\ +No validator found for real data media type +"application/vnd.apiary.http-headers+json" +and expected data media type +"null".` + ); + }); + }); + }); + + describe('body', () => { + it('has "JsonExample" validator', () => { + assert.propertyVal(res.body, 'validator', 'JsonExample'); + }); + + it('has "application/json" real type', () => { + assert.propertyVal(res.body, 'realType', 'application/json'); + }); + + it('has "application/json" expected type', () => { + assert.propertyVal(res.body, 'expectedType', 'application/json'); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(res.body.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.body.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.body.results[0], + 'message', + `At '' Invalid type: object (expected integer)` + ); + }); + }); + }); + }); +}); diff --git a/lib/api/validateRequest.js b/lib/api/validateRequest.js new file mode 100644 index 00000000..dd897a2c --- /dev/null +++ b/lib/api/validateRequest.js @@ -0,0 +1,11 @@ +const validateHeaders = require('./units/validateHeaders'); +const { validateBody } = require('./units/validateBody'); + +function validateRequest(real, expected) { + return { + headers: validateHeaders(real, expected), + body: validateBody(real, expected) + }; +} + +module.exports = validateRequest; From 1038ec88c0bdf3b00fb0886ed3dec64e75674c9d Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Wed, 15 May 2019 15:41:37 +0200 Subject: [PATCH 18/38] fix: fixes wrong strings interpolation in test suits --- test/unit/validate-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/validate-test.js b/test/unit/validate-test.js index 4e7961d7..c4557a89 100644 --- a/test/unit/validate-test.js +++ b/test/unit/validate-test.js @@ -45,7 +45,7 @@ describe('Gavel proxies to functions with callbacks', () => { describe('isValidatable', () => { describe('when I provide data from README (good objects)', () => { ['response', 'request'].forEach((variant) => { - describe('for two cloned #{variant}s', () => { + describe(`for two cloned ${variant}s`, () => { let results = null; let error = null; @@ -195,7 +195,7 @@ describe('Gavel proxies to functions with callbacks', () => { describe('validate', () => { describe('when I provide data', () => { ['response', 'request'].forEach((variant) => { - describe('for two cloned #{variant}s', () => { + describe(`for two cloned ${variant}s`, () => { let results = null; let error = null; @@ -217,7 +217,7 @@ describe('Gavel proxies to functions with callbacks', () => { it('should results be an object', () => assert.isObject(results)); }); - describe('for similar #{variant}s', () => { + describe(`for similar ${variant}s`, () => { let results = null; let error = null; @@ -239,7 +239,7 @@ describe('Gavel proxies to functions with callbacks', () => { it('should results be an object', () => assert.isObject(results)); }); - describe('for different #{variant}s', () => { + describe(`for different ${variant}s`, () => { let results = null; let error = null; From 46a5769e594e6a835616ccee0bf83a9faa1329ab Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 10:50:26 +0200 Subject: [PATCH 19/38] chore: updates to "gavel-spec@1.2.3" --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a38f22e..24be9a15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4023,9 +4023,9 @@ } }, "gavel-spec": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/gavel-spec/-/gavel-spec-1.2.1.tgz", - "integrity": "sha512-XTlQXl7CG/nhh2bNWocmbyTB1RACOBxNgaJcgynu47d9EFoXCGSsQKNjNxclv41cqI6fsifuttyTGZBHKzQIiQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/gavel-spec/-/gavel-spec-1.2.3.tgz", + "integrity": "sha512-cfxxJ5TTM1ojKBY7q4t0B/e7Q8i9AV7qlbuE2eKIPilmfy1Ct+lN/FM+U18s4iFFr9SC09nLNBnIb7qOBk2djg==", "dev": true }, "get-assigned-identifiers": { diff --git a/package.json b/package.json index 74a6958b..025c264c 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "eslint-config-airbnb-base": "13.1.0", "eslint-config-prettier": "4.3.0", "eslint-plugin-import": "2.17.2", - "gavel-spec": "1.2.1", + "gavel-spec": "1.2.3", "husky": "2.3.0", "lint-staged": "8.1.7", "mocha": "6.1.4", From 6eea40206f5f0de00db24ece4148e440ad13dd71 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 11:07:54 +0200 Subject: [PATCH 20/38] test: integrates new implementation into old test suits --- ...equest.test.js => validataElement.test.js} | 19 ++++++++----- lib/api/validate.js | 23 ++++++++++++++++ lib/api/validateElement.js | 27 +++++++++++++++++++ lib/api/validateRequest.js | 11 -------- lib/gavel.js | 3 ++- package.json | 2 +- test/unit/validate-test.js | 2 +- 7 files changed, 67 insertions(+), 20 deletions(-) rename lib/api/test/integration/{validateRequest.test.js => validataElement.test.js} (89%) create mode 100644 lib/api/validate.js create mode 100644 lib/api/validateElement.js delete mode 100644 lib/api/validateRequest.js diff --git a/lib/api/test/integration/validateRequest.test.js b/lib/api/test/integration/validataElement.test.js similarity index 89% rename from lib/api/test/integration/validateRequest.test.js rename to lib/api/test/integration/validataElement.test.js index 0fe83e77..a47898ed 100644 --- a/lib/api/test/integration/validateRequest.test.js +++ b/lib/api/test/integration/validataElement.test.js @@ -1,7 +1,7 @@ const { assert } = require('chai'); -const validateRequest = require('../../validateRequest'); +const validateElement = require('../../validateElement'); -describe('validateRequest', () => { +describe('validateElement', () => { describe('with matching requests', () => { const request = { method: 'POST', @@ -10,7 +10,7 @@ describe('validateRequest', () => { }, body: '{ "foo": "bar" }' }; - const res = validateRequest(request, request); + const res = validateElement(request, request); it('returns validation result object', () => { assert.isObject(res); @@ -66,7 +66,7 @@ describe('validateRequest', () => { }); describe('with non-matching requests', () => { - const res = validateRequest( + const res = validateElement( { method: 'POST', headers: { @@ -86,14 +86,17 @@ describe('validateRequest', () => { }); it('contains all validatable keys', () => { - assert.hasAllKeys(res, ['headers', 'body']); + // Note that "headers" are not present because + // Gavel demands a validatable key to be present + // in both real and expected elements. + assert.hasAllKeys(res, ['body']); }); describe('method', () => { it.skip('compares methods'); }); - describe('headers', () => { + describe.skip('headers', () => { it('has no validator set', () => { assert.propertyVal(res.headers, 'validator', null); }); @@ -165,4 +168,8 @@ and expected data media type }); }); }); + + describe('with matching responses', () => { + it.skip('add'); + }); }); diff --git a/lib/api/validate.js b/lib/api/validate.js new file mode 100644 index 00000000..4aca8531 --- /dev/null +++ b/lib/api/validate.js @@ -0,0 +1,23 @@ +const normalize = require('./units/normalize'); +const validateElement = require('./validateElement'); + +function validate(real, expected, type, callback) { + const normalizedReal = normalize(real); + const normalizedExpected = normalize(expected); + + if (type !== 'request' && type !== 'response') { + throw new Error( + `Can't validate: expected transaction "type" to be "request" or "response", but got: ${type}.` + ); + } + + const results = validateElement(normalizedReal, normalizedExpected); + + // TODO Propagate the error. + callback(null, { + version: '2', + ...results + }); +} + +module.exports = validate; diff --git a/lib/api/validateElement.js b/lib/api/validateElement.js new file mode 100644 index 00000000..29b71c99 --- /dev/null +++ b/lib/api/validateElement.js @@ -0,0 +1,27 @@ +const isset = require('../utils/isset'); +const validateStatusCode = require('./units/validateStatusCode'); +const validateHeaders = require('./units/validateHeaders'); +const { validateBody } = require('./units/validateBody'); + +function validateElement(real, expected) { + const results = {}; + + if (real.statusCode) { + results.statusCode = validateStatusCode(real, expected); + } + + if (real.headers && expected.headers) { + results.headers = validateHeaders(real, expected); + } + + if ( + isset(real.body) && + (isset(expected.body) || isset(expected.bodySchema)) + ) { + results.body = validateBody(real, expected); + } + + return results; +} + +module.exports = validateElement; diff --git a/lib/api/validateRequest.js b/lib/api/validateRequest.js deleted file mode 100644 index dd897a2c..00000000 --- a/lib/api/validateRequest.js +++ /dev/null @@ -1,11 +0,0 @@ -const validateHeaders = require('./units/validateHeaders'); -const { validateBody } = require('./units/validateBody'); - -function validateRequest(real, expected) { - return { - headers: validateHeaders(real, expected), - body: validateBody(real, expected) - }; -} - -module.exports = validateRequest; diff --git a/lib/gavel.js b/lib/gavel.js index 55c27c4e..b9b859f2 100644 --- a/lib/gavel.js +++ b/lib/gavel.js @@ -1,7 +1,8 @@ const { HttpRequest, ExpectedHttpRequest } = require('./model/http-request'); const { HttpResponse, ExpectedHttpResponse } = require('./model/http-response'); -const { validate, isValid, isValidatable } = require('./validate'); +const { isValid, isValidatable } = require('./validate'); +const validate = require('./api/validate'); module.exports = { HttpRequest, diff --git a/package.json b/package.json index 025c264c..539e588c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "scripts": { "lint": "eslint lib/**/*.js test/**/*.js", - "test": "npm run test:server && npm run test:browser && npm run test:features", + "test": "npm run test:new && npm run test:server && npm run test:browser && npm run test:features", "test:new": "mocha \"lib/api/test/**/*.test.js\"", "test:server": "mocha \"test/unit/**/*-test.js\"", "test:server:coverage": "nyc npm run test:server", diff --git a/test/unit/validate-test.js b/test/unit/validate-test.js index c4557a89..d1d6fd1a 100644 --- a/test/unit/validate-test.js +++ b/test/unit/validate-test.js @@ -1,6 +1,6 @@ const { assert } = require('chai'); const clone = require('clone'); -const { validate, isValid, isValidatable } = require('../../lib/validate'); +const { validate, isValid, isValidatable } = require('../../lib/gavel'); describe('Gavel proxies to functions with callbacks', () => { // Examples from README.md From 855a1d9a89fce8a52d8b5f9e3e866e16c36f3e84 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 11:14:12 +0200 Subject: [PATCH 21/38] refactor: ensures same export type of modules --- lib/api/test/integration/validataElement.test.js | 2 +- lib/api/test/unit/units/normalize/normalizeHeaders.test.js | 4 +++- lib/api/test/unit/units/validateHeaders.test.js | 2 +- lib/api/test/unit/units/validateStatusCode.test.js | 2 +- lib/api/units/normalize/index.js | 4 ++-- lib/api/units/normalize/normalizeHeaders.js | 2 +- lib/api/units/validateHeaders.js | 2 +- lib/api/units/validateStatusCode.js | 2 +- lib/api/validate.js | 4 ++-- lib/api/validateElement.js | 6 +++--- 10 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/api/test/integration/validataElement.test.js b/lib/api/test/integration/validataElement.test.js index a47898ed..d3ff47b5 100644 --- a/lib/api/test/integration/validataElement.test.js +++ b/lib/api/test/integration/validataElement.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateElement = require('../../validateElement'); +const { validateElement } = require('../../validateElement'); describe('validateElement', () => { describe('with matching requests', () => { diff --git a/lib/api/test/unit/units/normalize/normalizeHeaders.test.js b/lib/api/test/unit/units/normalize/normalizeHeaders.test.js index bad150a0..647a32e0 100644 --- a/lib/api/test/unit/units/normalize/normalizeHeaders.test.js +++ b/lib/api/test/unit/units/normalize/normalizeHeaders.test.js @@ -1,5 +1,7 @@ const { assert } = require('chai'); -const normalizeHeaders = require('../../../../units/normalize/normalizeHeaders'); +const { + normalizeHeaders +} = require('../../../../units/normalize/normalizeHeaders'); describe('normalizeHeaders', () => { describe('when given nothing', () => { diff --git a/lib/api/test/unit/units/validateHeaders.test.js b/lib/api/test/unit/units/validateHeaders.test.js index ab5b4b63..960f7d3f 100644 --- a/lib/api/test/unit/units/validateHeaders.test.js +++ b/lib/api/test/unit/units/validateHeaders.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateHeaders = require('../../../units/validateHeaders'); +const { validateHeaders } = require('../../../units/validateHeaders'); describe('validateHeaders', () => { describe('given matching headers', () => { diff --git a/lib/api/test/unit/units/validateStatusCode.test.js b/lib/api/test/unit/units/validateStatusCode.test.js index bc0b665e..e22cfbbe 100644 --- a/lib/api/test/unit/units/validateStatusCode.test.js +++ b/lib/api/test/unit/units/validateStatusCode.test.js @@ -1,5 +1,5 @@ const { assert } = require('chai'); -const validateStatusCode = require('../../../units/validateStatusCode'); +const { validateStatusCode } = require('../../../units/validateStatusCode'); describe('validateStatusCode', () => { describe('given matching status codes', () => { diff --git a/lib/api/units/normalize/index.js b/lib/api/units/normalize/index.js index cd84754c..a1bc2f1c 100644 --- a/lib/api/units/normalize/index.js +++ b/lib/api/units/normalize/index.js @@ -1,8 +1,8 @@ const evolve = require('../../utils/evolve'); -const normalizeHeaders = require('./normalizeHeaders'); +const { normalizeHeaders } = require('./normalizeHeaders'); const normalize = evolve({ headers: normalizeHeaders }); -module.exports = normalize; +module.exports = { normalize }; diff --git a/lib/api/units/normalize/normalizeHeaders.js b/lib/api/units/normalize/normalizeHeaders.js index 70a1e0c9..686a3a9e 100644 --- a/lib/api/units/normalize/normalizeHeaders.js +++ b/lib/api/units/normalize/normalizeHeaders.js @@ -32,4 +32,4 @@ const normalizeHeaders = (headers) => { ); }; -module.exports = normalizeHeaders; +module.exports = { normalizeHeaders }; diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js index 8d9bd439..a68b778b 100644 --- a/lib/api/units/validateHeaders.js +++ b/lib/api/units/validateHeaders.js @@ -64,4 +64,4 @@ and expected data media type }; } -module.exports = validateHeaders; +module.exports = { validateHeaders }; diff --git a/lib/api/units/validateStatusCode.js b/lib/api/units/validateStatusCode.js index 592cd86a..8b14ae4e 100644 --- a/lib/api/units/validateStatusCode.js +++ b/lib/api/units/validateStatusCode.js @@ -27,4 +27,4 @@ function validateStatusCode(real, expected) { }; } -module.exports = validateStatusCode; +module.exports = { validateStatusCode }; diff --git a/lib/api/validate.js b/lib/api/validate.js index 4aca8531..bc79434a 100644 --- a/lib/api/validate.js +++ b/lib/api/validate.js @@ -1,5 +1,5 @@ -const normalize = require('./units/normalize'); -const validateElement = require('./validateElement'); +const { normalize } = require('./units/normalize'); +const { validateElement } = require('./validateElement'); function validate(real, expected, type, callback) { const normalizedReal = normalize(real); diff --git a/lib/api/validateElement.js b/lib/api/validateElement.js index 29b71c99..5f71e891 100644 --- a/lib/api/validateElement.js +++ b/lib/api/validateElement.js @@ -1,6 +1,6 @@ const isset = require('../utils/isset'); -const validateStatusCode = require('./units/validateStatusCode'); -const validateHeaders = require('./units/validateHeaders'); +const { validateStatusCode } = require('./units/validateStatusCode'); +const { validateHeaders } = require('./units/validateHeaders'); const { validateBody } = require('./units/validateBody'); function validateElement(real, expected) { @@ -24,4 +24,4 @@ function validateElement(real, expected) { return results; } -module.exports = validateElement; +module.exports = { validateElement }; From 8ddfa93f4e9850c1bff3218b3ecbb91230f96043 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 14:24:01 +0200 Subject: [PATCH 22/38] test: adds response test cases to "validateElement" tests --- .../test/integration/validataElement.test.js | 175 --------- .../test/integration/validateElement.test.js | 366 ++++++++++++++++++ 2 files changed, 366 insertions(+), 175 deletions(-) delete mode 100644 lib/api/test/integration/validataElement.test.js create mode 100644 lib/api/test/integration/validateElement.test.js diff --git a/lib/api/test/integration/validataElement.test.js b/lib/api/test/integration/validataElement.test.js deleted file mode 100644 index d3ff47b5..00000000 --- a/lib/api/test/integration/validataElement.test.js +++ /dev/null @@ -1,175 +0,0 @@ -const { assert } = require('chai'); -const { validateElement } = require('../../validateElement'); - -describe('validateElement', () => { - describe('with matching requests', () => { - const request = { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: '{ "foo": "bar" }' - }; - const res = validateElement(request, request); - - it('returns validation result object', () => { - assert.isObject(res); - }); - - it('contains all validatable keys', () => { - assert.hasAllKeys(res, ['headers', 'body']); - }); - - describe('headers', () => { - it('has "HeadersJsonExample" validator', () => { - assert.propertyVal(res.headers, 'validator', 'HeadersJsonExample'); - }); - - it('has "application/vnd.apiary.http-headers+json" real headers type', () => { - assert.propertyVal( - res.headers, - 'realType', - 'application/vnd.apiary.http-headers+json' - ); - }); - - it('has "application/vnd.apiary.http-headers+json" expected headers type', () => { - assert.propertyVal( - res.headers, - 'expectedType', - 'application/vnd.apiary.http-headers+json' - ); - }); - - it('has no errors', () => { - assert.lengthOf(res.headers.results, 0); - }); - }); - - describe('body', () => { - it('has "JsonExample" validator', () => { - assert.propertyVal(res.body, 'validator', 'JsonExample'); - }); - - it('has "application/json" real body type', () => { - assert.propertyVal(res.body, 'realType', 'application/json'); - }); - - it('has "application/json" expected body type', () => { - assert.propertyVal(res.body, 'expectedType', 'application/json'); - }); - - it('has no errors', () => { - assert.lengthOf(res.body.results, 0); - }); - }); - }); - - describe('with non-matching requests', () => { - const res = validateElement( - { - method: 'POST', - headers: { - 'content-type': 'application/json' - }, - body: '{ "foo": "bar" }' - }, - { - method: 'PUT', - headers: null, - body: '2' - } - ); - - it('returns validation result object', () => { - assert.isObject(res); - }); - - it('contains all validatable keys', () => { - // Note that "headers" are not present because - // Gavel demands a validatable key to be present - // in both real and expected elements. - assert.hasAllKeys(res, ['body']); - }); - - describe('method', () => { - it.skip('compares methods'); - }); - - describe.skip('headers', () => { - it('has no validator set', () => { - assert.propertyVal(res.headers, 'validator', null); - }); - - it('has "application/vnd.apiary.http-headers+json" as real headers type', () => { - assert.propertyVal( - res.headers, - 'realType', - 'application/vnd.apiary.http-headers+json' - ); - }); - - it('has no expected headers type', () => { - assert.propertyVal(res.headers, 'expectedType', null); - }); - - describe('produces an error', () => { - it('exactly one error', () => { - assert.lengthOf(res.headers.results, 1); - }); - - it('has "error" severity', () => { - assert.propertyVal(res.headers.results[0], 'severity', 'error'); - }); - - it('has explanatory message', () => { - assert.propertyVal( - res.headers.results[0], - 'message', - `\ -No validator found for real data media type -"application/vnd.apiary.http-headers+json" -and expected data media type -"null".` - ); - }); - }); - }); - - describe('body', () => { - it('has "JsonExample" validator', () => { - assert.propertyVal(res.body, 'validator', 'JsonExample'); - }); - - it('has "application/json" real type', () => { - assert.propertyVal(res.body, 'realType', 'application/json'); - }); - - it('has "application/json" expected type', () => { - assert.propertyVal(res.body, 'expectedType', 'application/json'); - }); - - describe('produces an error', () => { - it('exactly one error', () => { - assert.lengthOf(res.body.results, 1); - }); - - it('has "error" severity', () => { - assert.propertyVal(res.body.results[0], 'severity', 'error'); - }); - - it('has explanatory message', () => { - assert.propertyVal( - res.body.results[0], - 'message', - `At '' Invalid type: object (expected integer)` - ); - }); - }); - }); - }); - - describe('with matching responses', () => { - it.skip('add'); - }); -}); diff --git a/lib/api/test/integration/validateElement.test.js b/lib/api/test/integration/validateElement.test.js new file mode 100644 index 00000000..7014c29f --- /dev/null +++ b/lib/api/test/integration/validateElement.test.js @@ -0,0 +1,366 @@ +const { assert } = require('chai'); +const { validateElement } = require('../../validateElement'); + +describe('validateElement', () => { + describe('with matching requests', () => { + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: '{ "foo": "bar" }' + }; + const res = validateElement(request, request); + + it('returns validation result object', () => { + assert.isObject(res); + }); + + it('contains all validatable keys', () => { + assert.hasAllKeys(res, ['headers', 'body']); + }); + + describe('headers', () => { + it('has "HeadersJsonExample" validator', () => { + assert.propertyVal(res.headers, 'validator', 'HeadersJsonExample'); + }); + + it('has "application/vnd.apiary.http-headers+json" real headers type', () => { + assert.propertyVal( + res.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has "application/vnd.apiary.http-headers+json" expected headers type', () => { + assert.propertyVal( + res.headers, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no errors', () => { + assert.lengthOf(res.headers.results, 0); + }); + }); + + describe('body', () => { + it('has "JsonExample" validator', () => { + assert.propertyVal(res.body, 'validator', 'JsonExample'); + }); + + it('has "application/json" real body type', () => { + assert.propertyVal(res.body, 'realType', 'application/json'); + }); + + it('has "application/json" expected body type', () => { + assert.propertyVal(res.body, 'expectedType', 'application/json'); + }); + + it('has no errors', () => { + assert.lengthOf(res.body.results, 0); + }); + }); + }); + + describe('with non-matching requests', () => { + const res = validateElement( + { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: '{ "foo": "bar" }' + }, + { + method: 'PUT', + headers: null, + body: '2' + } + ); + + it('returns validation result object', () => { + assert.isObject(res); + }); + + it('contains all validatable keys', () => { + // Note that "headers" are not present because + // Gavel demands a validatable key to be present + // in both real and expected elements. + assert.hasAllKeys(res, ['body']); + }); + + describe('method', () => { + it.skip('compares methods'); + }); + + describe.skip('headers', () => { + it('has no validator set', () => { + assert.propertyVal(res.headers, 'validator', null); + }); + + it('has "application/vnd.apiary.http-headers+json" as real headers type', () => { + assert.propertyVal( + res.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no expected headers type', () => { + assert.propertyVal(res.headers, 'expectedType', null); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(res.headers.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.headers.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.headers.results[0], + 'message', + `\ +No validator found for real data media type +"application/vnd.apiary.http-headers+json" +and expected data media type +"null".` + ); + }); + }); + }); + + describe('body', () => { + it('has "JsonExample" validator', () => { + assert.propertyVal(res.body, 'validator', 'JsonExample'); + }); + + it('has "application/json" real type', () => { + assert.propertyVal(res.body, 'realType', 'application/json'); + }); + + it('has "application/json" expected type', () => { + assert.propertyVal(res.body, 'expectedType', 'application/json'); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(res.body.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.body.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.body.results[0], + 'message', + `At '' Invalid type: object (expected integer)` + ); + }); + }); + }); + }); + + describe('with matching responses', () => { + const response = { + statusCode: 200, + headers: { + 'Content-Type': 'application/json' + }, + body: '{ "foo": "bar" }' + }; + const result = validateElement(response, response); + + it('returns validation result object', () => { + assert.isObject(result); + }); + + it('contains all validatable keys', () => { + assert.hasAllKeys(result, ['statusCode', 'headers', 'body']); + }); + + describe('statusCode', () => { + it('has "TextDiff" validator', () => { + assert.propertyVal(result.statusCode, 'validator', 'TextDiff'); + }); + + it('has "text/vnd.apiary.status-code" real type', () => { + assert.propertyVal( + result.statusCode, + 'realType', + 'text/vnd.apiary.status-code' + ); + }); + + it('has "text/vnd.apiary.status-code" expected type', () => { + assert.propertyVal( + result.statusCode, + 'expectedType', + 'text/vnd.apiary.status-code' + ); + }); + + it('has no errors', () => { + assert.lengthOf(result.statusCode.results, 0); + }); + }); + + describe('headers', () => { + it('has "HeadersJsonExample" validator', () => { + assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample'); + }); + + it('has "application/vnd.apiary.http-headers+json" real type', () => { + assert.propertyVal( + result.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has "application/vnd.apiary.http-headers+json" expected type', () => { + assert.propertyVal( + result.headers, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has no errors', () => { + assert.lengthOf(result.headers.results, 0); + }); + }); + + describe('body', () => { + it('has "JsonExample" validator', () => { + assert.propertyVal(result.body, 'validator', 'JsonExample'); + }); + + it('has "application/json" real type', () => { + assert.propertyVal(result.body, 'realType', 'application/json'); + }); + + it('has "application/json" expected type', () => { + assert.propertyVal(result.body, 'expectedType', 'application/json'); + }); + + it('has no errors', () => { + assert.lengthOf(result.body.results, 0); + }); + }); + }); + + describe('with non-matching responses', () => { + const realResponse = { + statusCode: 400, + headers: { + 'Content-Type': 'application/json' + } + }; + const expectedResponse = { + statusCode: 200, + headers: { + 'Accept-Language': 'en-US' + } + }; + const result = validateElement(realResponse, expectedResponse); + + it('returns validation result object', () => { + assert.isObject(result); + }); + + it('contains all validatable keys', () => { + assert.hasAllKeys(result, ['statusCode', 'headers']); + }); + + console.log(result); + + describe('statusCode', () => { + it('has "TextDiff" validator', () => { + assert.propertyVal(result.statusCode, 'validator', 'TextDiff'); + }); + + it('has "text/vnd.apiary.status-code" real type', () => { + assert.propertyVal( + result.statusCode, + 'realType', + 'text/vnd.apiary.status-code' + ); + }); + + it('has "text/vnd.apiary.status-code" expected type', () => { + assert.propertyVal( + result.statusCode, + 'expectedType', + 'text/vnd.apiary.status-code' + ); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(result.statusCode.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(result.statusCode.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + result.statusCode.results[0], + 'message', + 'Real and expected data does not match.' + ); + }); + }); + }); + + describe('headers', () => { + it('has "HeadersJsonExample" validator', () => { + assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample'); + }); + + it('has "application/vnd.apiary.http-headers+json" real type', () => { + assert.propertyVal( + result.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + it('has "application/vnd.apiary.http-headers+json" real type', () => { + assert.propertyVal( + result.headers, + 'realType', + 'application/vnd.apiary.http-headers+json' + ); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(result.headers.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(result.headers.results[0], 'severity', 'error'); + }); + + it('includes missing header in the message', () => { + assert.propertyVal( + result.headers.results[0], + 'message', + `At '/accept-language' Missing required property: accept-language` + ); + }); + }); + }); + }); +}); From e5c6deadfbe3b9e4af58e589f940466266435505 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 14:40:51 +0200 Subject: [PATCH 23/38] refactor: propagates internal exception to given callback --- lib/api/validate.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/api/validate.js b/lib/api/validate.js index bc79434a..031cfc02 100644 --- a/lib/api/validate.js +++ b/lib/api/validate.js @@ -11,13 +11,15 @@ function validate(real, expected, type, callback) { ); } - const results = validateElement(normalizedReal, normalizedExpected); - - // TODO Propagate the error. - callback(null, { - version: '2', - ...results - }); + try { + const results = validateElement(normalizedReal, normalizedExpected); + callback(null, { + version: '2', + ...results + }); + } catch (error) { + callback(error, null); + } } module.exports = validate; From cf7342559a778263482b880e0401f49773996fd4 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 14:56:17 +0200 Subject: [PATCH 24/38] refactor: removes trimming of header values --- lib/api/units/normalize/normalizeHeaders.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/units/normalize/normalizeHeaders.js b/lib/api/units/normalize/normalizeHeaders.js index 686a3a9e..a663fec5 100644 --- a/lib/api/units/normalize/normalizeHeaders.js +++ b/lib/api/units/normalize/normalizeHeaders.js @@ -1,5 +1,5 @@ const normalizeStringValue = (value) => { - return value.toLowerCase().trim(); + return value.toLowerCase(); }; /** From 6c969301d31e39d5dbfdc0e7ec01b1a4083eb195 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 14:58:36 +0200 Subject: [PATCH 25/38] refactor: removes try/catch blocks in "isJsonContentType" --- lib/api/units/validateBody.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index 1bbcb38b..a1d96442 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -53,14 +53,8 @@ function parseContentType(contentType) { * @returns {boolean} */ function isJsonContentType(contentType) { - try { - const mediaType = parseContentType(contentType); - return isJson(mediaType); - } catch (error) { - // Silence an error on purporse because contentType - // may contain any kind of rubbish. - return false; - } + const mediaType = parseContentType(contentType); + return mediaType ? isJson(mediaType) : false; } /** From 0bd7607bbe2eb09e9396afb898d0c90dd14b3101 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 15:31:50 +0200 Subject: [PATCH 26/38] test: ensures proper return type of "evolve" --- lib/api/test/unit/utils/evolve.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/api/test/unit/utils/evolve.test.js b/lib/api/test/unit/utils/evolve.test.js index 77398a7f..8c759a1b 100644 --- a/lib/api/test/unit/utils/evolve.test.js +++ b/lib/api/test/unit/utils/evolve.test.js @@ -20,6 +20,10 @@ describe('evolve', () => { b: 'foo' }); + it('returns object', () => { + assert.isObject(res); + }); + it('evolves matching properties', () => { assert.propertyVal(res, 'a', 4); }); @@ -40,6 +44,10 @@ describe('evolve', () => { 3: multiply(4) })([1, 2, 3]); + it('returns array', () => { + assert.isArray(res); + }); + it('evolves matching keys', () => { assert.propertyVal(res, 0, 2); assert.propertyVal(res, 1, 6); From 7d50b139c920af594cd1d5db79acd78a7d591069 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 15:35:01 +0200 Subject: [PATCH 27/38] refactor: renames "validateElement" to "validateMessage" --- ...lement.test.js => validateMessage.test.js} | 62 +++++++++---------- lib/api/validate.js | 4 +- ...{validateElement.js => validateMessage.js} | 4 +- 3 files changed, 34 insertions(+), 36 deletions(-) rename lib/api/test/integration/{validateElement.test.js => validateMessage.test.js} (84%) rename lib/api/{validateElement.js => validateMessage.js} (88%) diff --git a/lib/api/test/integration/validateElement.test.js b/lib/api/test/integration/validateMessage.test.js similarity index 84% rename from lib/api/test/integration/validateElement.test.js rename to lib/api/test/integration/validateMessage.test.js index 7014c29f..243e7203 100644 --- a/lib/api/test/integration/validateElement.test.js +++ b/lib/api/test/integration/validateMessage.test.js @@ -1,7 +1,7 @@ const { assert } = require('chai'); -const { validateElement } = require('../../validateElement'); +const { validateMessage } = require('../../validateMessage'); -describe('validateElement', () => { +describe('validateMessage', () => { describe('with matching requests', () => { const request = { method: 'POST', @@ -10,24 +10,24 @@ describe('validateElement', () => { }, body: '{ "foo": "bar" }' }; - const res = validateElement(request, request); + const result = validateMessage(request, request); it('returns validation result object', () => { - assert.isObject(res); + assert.isObject(result); }); it('contains all validatable keys', () => { - assert.hasAllKeys(res, ['headers', 'body']); + assert.hasAllKeys(result, ['headers', 'body']); }); describe('headers', () => { it('has "HeadersJsonExample" validator', () => { - assert.propertyVal(res.headers, 'validator', 'HeadersJsonExample'); + assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample'); }); it('has "application/vnd.apiary.http-headers+json" real headers type', () => { assert.propertyVal( - res.headers, + result.headers, 'realType', 'application/vnd.apiary.http-headers+json' ); @@ -35,38 +35,38 @@ describe('validateElement', () => { it('has "application/vnd.apiary.http-headers+json" expected headers type', () => { assert.propertyVal( - res.headers, + result.headers, 'expectedType', 'application/vnd.apiary.http-headers+json' ); }); it('has no errors', () => { - assert.lengthOf(res.headers.results, 0); + assert.lengthOf(result.headers.results, 0); }); }); describe('body', () => { it('has "JsonExample" validator', () => { - assert.propertyVal(res.body, 'validator', 'JsonExample'); + assert.propertyVal(result.body, 'validator', 'JsonExample'); }); it('has "application/json" real body type', () => { - assert.propertyVal(res.body, 'realType', 'application/json'); + assert.propertyVal(result.body, 'realType', 'application/json'); }); it('has "application/json" expected body type', () => { - assert.propertyVal(res.body, 'expectedType', 'application/json'); + assert.propertyVal(result.body, 'expectedType', 'application/json'); }); it('has no errors', () => { - assert.lengthOf(res.body.results, 0); + assert.lengthOf(result.body.results, 0); }); }); }); describe('with non-matching requests', () => { - const res = validateElement( + const result = validateMessage( { method: 'POST', headers: { @@ -82,14 +82,14 @@ describe('validateElement', () => { ); it('returns validation result object', () => { - assert.isObject(res); + assert.isObject(result); }); it('contains all validatable keys', () => { // Note that "headers" are not present because // Gavel demands a validatable key to be present // in both real and expected elements. - assert.hasAllKeys(res, ['body']); + assert.hasAllKeys(result, ['body']); }); describe('method', () => { @@ -98,33 +98,33 @@ describe('validateElement', () => { describe.skip('headers', () => { it('has no validator set', () => { - assert.propertyVal(res.headers, 'validator', null); + assert.propertyVal(result.headers, 'validator', null); }); it('has "application/vnd.apiary.http-headers+json" as real headers type', () => { assert.propertyVal( - res.headers, + result.headers, 'realType', 'application/vnd.apiary.http-headers+json' ); }); it('has no expected headers type', () => { - assert.propertyVal(res.headers, 'expectedType', null); + assert.propertyVal(result.headers, 'expectedType', null); }); describe('produces an error', () => { it('exactly one error', () => { - assert.lengthOf(res.headers.results, 1); + assert.lengthOf(result.headers.results, 1); }); it('has "error" severity', () => { - assert.propertyVal(res.headers.results[0], 'severity', 'error'); + assert.propertyVal(result.headers.results[0], 'severity', 'error'); }); it('has explanatory message', () => { assert.propertyVal( - res.headers.results[0], + result.headers.results[0], 'message', `\ No validator found for real data media type @@ -138,29 +138,29 @@ and expected data media type describe('body', () => { it('has "JsonExample" validator', () => { - assert.propertyVal(res.body, 'validator', 'JsonExample'); + assert.propertyVal(result.body, 'validator', 'JsonExample'); }); it('has "application/json" real type', () => { - assert.propertyVal(res.body, 'realType', 'application/json'); + assert.propertyVal(result.body, 'realType', 'application/json'); }); it('has "application/json" expected type', () => { - assert.propertyVal(res.body, 'expectedType', 'application/json'); + assert.propertyVal(result.body, 'expectedType', 'application/json'); }); describe('produces an error', () => { it('exactly one error', () => { - assert.lengthOf(res.body.results, 1); + assert.lengthOf(result.body.results, 1); }); it('has "error" severity', () => { - assert.propertyVal(res.body.results[0], 'severity', 'error'); + assert.propertyVal(result.body.results[0], 'severity', 'error'); }); it('has explanatory message', () => { assert.propertyVal( - res.body.results[0], + result.body.results[0], 'message', `At '' Invalid type: object (expected integer)` ); @@ -177,7 +177,7 @@ and expected data media type }, body: '{ "foo": "bar" }' }; - const result = validateElement(response, response); + const result = validateMessage(response, response); it('returns validation result object', () => { assert.isObject(result); @@ -271,7 +271,7 @@ and expected data media type 'Accept-Language': 'en-US' } }; - const result = validateElement(realResponse, expectedResponse); + const result = validateMessage(realResponse, expectedResponse); it('returns validation result object', () => { assert.isObject(result); @@ -281,8 +281,6 @@ and expected data media type assert.hasAllKeys(result, ['statusCode', 'headers']); }); - console.log(result); - describe('statusCode', () => { it('has "TextDiff" validator', () => { assert.propertyVal(result.statusCode, 'validator', 'TextDiff'); diff --git a/lib/api/validate.js b/lib/api/validate.js index 031cfc02..a6ae521f 100644 --- a/lib/api/validate.js +++ b/lib/api/validate.js @@ -1,5 +1,5 @@ const { normalize } = require('./units/normalize'); -const { validateElement } = require('./validateElement'); +const { validateMessage } = require('./validateMessage'); function validate(real, expected, type, callback) { const normalizedReal = normalize(real); @@ -12,7 +12,7 @@ function validate(real, expected, type, callback) { } try { - const results = validateElement(normalizedReal, normalizedExpected); + const results = validateMessage(normalizedReal, normalizedExpected); callback(null, { version: '2', ...results diff --git a/lib/api/validateElement.js b/lib/api/validateMessage.js similarity index 88% rename from lib/api/validateElement.js rename to lib/api/validateMessage.js index 5f71e891..8f7d4e37 100644 --- a/lib/api/validateElement.js +++ b/lib/api/validateMessage.js @@ -3,7 +3,7 @@ const { validateStatusCode } = require('./units/validateStatusCode'); const { validateHeaders } = require('./units/validateHeaders'); const { validateBody } = require('./units/validateBody'); -function validateElement(real, expected) { +function validateMessage(real, expected) { const results = {}; if (real.statusCode) { @@ -24,4 +24,4 @@ function validateElement(real, expected) { return results; } -module.exports = { validateElement }; +module.exports = { validateMessage }; From c8018ea7b1be4276487e9d00f0855ea381be2f4b Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 15:42:51 +0200 Subject: [PATCH 28/38] refactor: prevents a call to callback within try/catch block --- lib/api/validate.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/api/validate.js b/lib/api/validate.js index a6ae521f..f90336d1 100644 --- a/lib/api/validate.js +++ b/lib/api/validate.js @@ -11,15 +11,19 @@ function validate(real, expected, type, callback) { ); } + let results; + try { - const results = validateMessage(normalizedReal, normalizedExpected); - callback(null, { - version: '2', - ...results - }); + results = validateMessage(normalizedReal, normalizedExpected); } catch (error) { callback(error, null); + return; } + + callback(null, { + version: '2', + ...results + }); } module.exports = validate; From c4a71c081d3cf9c268eba666f42e131f142f17a6 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 15:48:46 +0200 Subject: [PATCH 29/38] refactor: performs normalization of http messages in "validateMessage" --- lib/api/test/integration/validateMessage.test.js | 9 +++------ lib/api/validate.js | 6 +----- lib/api/validateMessage.js | 5 ++++- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/api/test/integration/validateMessage.test.js b/lib/api/test/integration/validateMessage.test.js index 243e7203..d71bef8b 100644 --- a/lib/api/test/integration/validateMessage.test.js +++ b/lib/api/test/integration/validateMessage.test.js @@ -72,11 +72,11 @@ describe('validateMessage', () => { headers: { 'content-type': 'application/json' }, - body: '{ "foo": "bar" }' + body: '{ "lookHere": "foo" }' }, { method: 'PUT', - headers: null, + headers: '', body: '2' } ); @@ -86,10 +86,7 @@ describe('validateMessage', () => { }); it('contains all validatable keys', () => { - // Note that "headers" are not present because - // Gavel demands a validatable key to be present - // in both real and expected elements. - assert.hasAllKeys(result, ['body']); + assert.hasAllKeys(result, ['headers', 'body']); }); describe('method', () => { diff --git a/lib/api/validate.js b/lib/api/validate.js index f90336d1..b149fa00 100644 --- a/lib/api/validate.js +++ b/lib/api/validate.js @@ -1,10 +1,6 @@ -const { normalize } = require('./units/normalize'); const { validateMessage } = require('./validateMessage'); function validate(real, expected, type, callback) { - const normalizedReal = normalize(real); - const normalizedExpected = normalize(expected); - if (type !== 'request' && type !== 'response') { throw new Error( `Can't validate: expected transaction "type" to be "request" or "response", but got: ${type}.` @@ -14,7 +10,7 @@ function validate(real, expected, type, callback) { let results; try { - results = validateMessage(normalizedReal, normalizedExpected); + results = validateMessage(real, expected); } catch (error) { callback(error, null); return; diff --git a/lib/api/validateMessage.js b/lib/api/validateMessage.js index 8f7d4e37..172d4582 100644 --- a/lib/api/validateMessage.js +++ b/lib/api/validateMessage.js @@ -1,9 +1,12 @@ const isset = require('../utils/isset'); +const { normalize } = require('./units/normalize'); const { validateStatusCode } = require('./units/validateStatusCode'); const { validateHeaders } = require('./units/validateHeaders'); const { validateBody } = require('./units/validateBody'); -function validateMessage(real, expected) { +function validateMessage(realMessage, expectedMessage) { + const real = normalize(realMessage); + const expected = normalize(expectedMessage); const results = {}; if (real.statusCode) { From 85b6c2292e04a9443e8cd4eaa014caf09f7838e7 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 15:58:25 +0200 Subject: [PATCH 30/38] refactor: simplifies "isset" util --- lib/utils/isset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/isset.js b/lib/utils/isset.js index 4ea1ae64..b3384fe3 100644 --- a/lib/utils/isset.js +++ b/lib/utils/isset.js @@ -1,5 +1,5 @@ function isset(value) { - return typeof value !== 'undefined' && value !== null; + return value != null; } module.exports = isset; From c77a427c8407c0a0da369493074a6b3cbe5f730c Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Thu, 16 May 2019 16:24:09 +0200 Subject: [PATCH 31/38] refactor: parameterize http message origin of "getBodyType" --- lib/api/test/unit/units/validateBody.test.js | 4 ++-- .../validateBody/getBodySchemaType.test.js | 2 +- .../units/validateBody/getBodyType.test.js | 10 ++++----- lib/api/units/validateBody.js | 22 ++++++++----------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/api/test/unit/units/validateBody.test.js b/lib/api/test/unit/units/validateBody.test.js index 0cc6a2fc..9b8b7518 100644 --- a/lib/api/test/unit/units/validateBody.test.js +++ b/lib/api/test/unit/units/validateBody.test.js @@ -72,7 +72,7 @@ describe('validateBody', () => { it('has explanatory message', () => { assert.match( res.results[0].message, - /^Real body 'Content-Type' header is 'application\/json' but body is not a parseable JSON:/ + /^Can't validate: real body 'Content-Type' header is 'application\/json' but body is not a parseable JSON:/ ); }); }); @@ -142,7 +142,7 @@ describe('validateBody', () => { it('has explanatory message', () => { assert.match( res.results[0].message, - /^Real body 'Content-Type' header is 'application\/hal\+json' but body is not a parseable JSON:/ + /^Can't validate: real body 'Content-Type' header is 'application\/hal\+json' but body is not a parseable JSON:/ ); }); }); diff --git a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js index 06e5f9e4..edd400db 100644 --- a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js +++ b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js @@ -39,7 +39,7 @@ describe('getBodySchemaType', () => { it('returns parsing error', () => { assert.match( res[0], - /^Can't validate. Expected body JSON Schema is not a parseable JSON:/ + /^Can't validate: expected body JSON Schema is not a parseable JSON:/ ); }); diff --git a/lib/api/test/unit/units/validateBody/getBodyType.test.js b/lib/api/test/unit/units/validateBody/getBodyType.test.js index 1301d2d8..4d58786a 100644 --- a/lib/api/test/unit/units/validateBody/getBodyType.test.js +++ b/lib/api/test/unit/units/validateBody/getBodyType.test.js @@ -10,7 +10,7 @@ describe('getBodyType', () => { jsonTypes.forEach((jsonType) => { describe(`when given "${jsonType}" content type`, () => { describe('and parsable json body', () => { - const res = getBodyType('{ "foo": "bar" }', jsonType); + const res = getBodyType('{ "foo": "bar" }', jsonType, 'real'); it('returns no errors', () => { assert.isNull(res[0]); @@ -22,14 +22,14 @@ describe('getBodyType', () => { }); describe('and non-parsable json body', () => { - const res = getBodyType('abc', jsonType); + const res = getBodyType('abc', jsonType, 'real'); it('returns parsing error', () => { assert.match( res[0], new RegExp( /* eslint-disable no-useless-escape */ - `^Real body 'Content-Type' header is \'${jsonType.replace( + `^Can't validate: real body 'Content-Type' header is \'${jsonType.replace( /(\/|\+)/g, '\\$1' )}\' but body is not a parseable JSON:` @@ -50,7 +50,7 @@ describe('getBodyType', () => { nonJsonTypes.forEach((nonJsonType) => { describe(`when given "${nonJsonType}" content type`, () => { describe('and parsable json body', () => { - const res = getBodyType('{ "foo": "bar" }', nonJsonType); + const res = getBodyType('{ "foo": "bar" }', nonJsonType, 'real'); it('returns no errors', () => { assert.isNull(res[0]); @@ -62,7 +62,7 @@ describe('getBodyType', () => { }); describe('and a non-json body', () => { - const res = getBodyType('abc', nonJsonType); + const res = getBodyType('abc', nonJsonType, 'real'); it('returns no errors', () => { assert.isNull(res[0]); diff --git a/lib/api/units/validateBody.js b/lib/api/units/validateBody.js index a1d96442..71718354 100644 --- a/lib/api/units/validateBody.js +++ b/lib/api/units/validateBody.js @@ -62,9 +62,10 @@ function isJsonContentType(contentType) { * on the given body and normalized headers. * @param {string} body * @param {Object} headers + * @param {'real'|'expected'} bodyType * @returns {[error, bodyType]} */ -function getBodyType(body, contentType) { +function getBodyType(body, contentType, httpMessageOrigin) { const hasJsonContentType = isJsonContentType(contentType); try { @@ -77,16 +78,9 @@ function getBodyType(body, contentType) { } catch (parsingError) { const fallbackMediaType = mediaTyper.parse('text/plain'); const error = hasJsonContentType - ? /** - * @TODO The same message for real/expected - * body assertion. Real/expected must be reflected - * in the error message. - */ - `\ -Real body 'Content-Type' header is '${contentType}' \ + ? `Can't validate: ${httpMessageOrigin} body 'Content-Type' header is '${contentType}' \ but body is not a parseable JSON: -${parsingError.message}\ - ` +${parsingError.message}` : null; return [error, fallbackMediaType]; @@ -110,7 +104,7 @@ function getBodySchemaType(bodySchema) { jph.parse(bodySchema); return [null, jsonSchemaType]; } catch (exception) { - const error = `Can't validate. Expected body JSON Schema is not a parseable JSON:\n${ + const error = `Can't validate: expected body JSON Schema is not a parseable JSON:\n${ exception.message }`; @@ -165,13 +159,15 @@ function validateBody(real, expected) { const [realTypeError, realType] = getBodyType( real.body, - real.headers && real.headers['content-type'] + real.headers && real.headers['content-type'], + 'real' ); const [expectedTypeError, expectedType] = expected.bodySchema ? getBodySchemaType(expected.bodySchema) : getBodyType( expected.body, - expected.headers && expected.headers['content-type'] + expected.headers && expected.headers['content-type'], + 'expected' ); if (realTypeError) { From bd2a9063ef8f1adcf41b31aad75f1644c84decc7 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 17 May 2019 11:20:29 +0200 Subject: [PATCH 32/38] test: asserts each missing header in "validateHeaders" test --- .../test/unit/units/validateHeaders.test.js | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/api/test/unit/units/validateHeaders.test.js b/lib/api/test/unit/units/validateHeaders.test.js index 960f7d3f..804da20a 100644 --- a/lib/api/test/unit/units/validateHeaders.test.js +++ b/lib/api/test/unit/units/validateHeaders.test.js @@ -18,11 +18,11 @@ describe('validateHeaders', () => { } ); - it('has proper validator', () => { + it('has "HeadersJsonExample" validator', () => { assert.propertyVal(res, 'validator', 'HeadersJsonExample'); }); - it('has proper real type', () => { + it('has "application/vnd.apiary.http-headers+json" real type', () => { assert.propertyVal( res, 'realType', @@ -30,7 +30,7 @@ describe('validateHeaders', () => { ); }); - it('has proper expected type', () => { + it('has "application/vnd.apiary.http-headers+json" expected type', () => { assert.propertyVal( res, 'expectedType', @@ -52,17 +52,18 @@ describe('validateHeaders', () => { }, { headers: { + 'accept-language': 'en-US,us', 'content-type': 'application/json', connection: 'keep-alive' } } ); - it('has proper validator', () => { + it('has "HeadersJsonExample" validator', () => { assert.propertyVal(res, 'validator', 'HeadersJsonExample'); }); - it('has proper real type', () => { + it('has "application/vnd.apiary.http-headers+json" real type', () => { assert.propertyVal( res, 'realType', @@ -70,7 +71,7 @@ describe('validateHeaders', () => { ); }); - it('has proper expected type', () => { + it('has "application/vnd.apiary.http-headers+json" expected type', () => { assert.propertyVal( res, 'expectedType', @@ -78,25 +79,37 @@ describe('validateHeaders', () => { ); }); - describe('has error', () => { - it('for each missing header', () => { - assert.lengthOf(res.results, 1); - }); + describe('produces errors', () => { + const missingHeaders = ['accept-language', 'content-type']; - it('contains pointer to missing header(s)', () => { - assert.propertyVal(res.results[0], 'pointer', '/content-type'); + it('for two missing headers', () => { + assert.lengthOf(res.results, missingHeaders.length); }); - it('contains error message', () => { - assert.propertyVal( - res.results[0], - 'message', - `At '/content-type' Missing required property: content-type` - ); - }); - - it('contains proper severity', () => { - assert.propertyVal(res.results[0], 'severity', 'error'); + describe('for each missing header', () => { + missingHeaders.forEach((headerName, index) => { + describe(headerName, () => { + it('has "error" severity', () => { + assert.propertyVal(res.results[index], 'severity', 'error'); + }); + + it('has pointer to header name', () => { + assert.propertyVal( + res.results[index], + 'pointer', + `/${headerName}` + ); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.results[index], + 'message', + `At '/${headerName}' Missing required property: ${headerName}` + ); + }); + }); + }); }); }); }); @@ -123,17 +136,25 @@ describe('validateHeaders', () => { assert.propertyVal(res, 'expectedType', null); }); - it('has invalid format error', () => { - assert.lengthOf(res.results, 1); - assert.propertyVal(res.results[0], 'severity', 'error'); - assert.propertyVal( - res.results[0], - 'message', - `No validator found for real data media type + describe('produces an error', () => { + it('has one error', () => { + assert.lengthOf(res.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('has exlpanatory message', () => { + assert.propertyVal( + res.results[0], + 'message', + `No validator found for real data media type "null" and expected data media type "null".` - ); + ); + }); }); }); }); From 38776ba4a0fe1ada1ad376b410868046929e8a5f Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 17 May 2019 14:45:31 +0200 Subject: [PATCH 33/38] test: polishes the test suits --- .../test/integration/validateMessage.test.js | 39 +++---- lib/api/test/unit/units/validateBody.test.js | 107 +++++++++--------- .../validateBody/getBodySchemaType.test.js | 67 +++++++---- .../units/validateBody/getBodyType.test.js | 10 +- .../validateBody/getBodyValidator.test.js | 20 ++++ .../unit/units/validateBody/isJson.test.js | 6 +- .../units/validateBody/isJsonSchema.test.js | 2 +- .../unit/units/validateStatusCode.test.js | 35 +++--- 8 files changed, 164 insertions(+), 122 deletions(-) diff --git a/lib/api/test/integration/validateMessage.test.js b/lib/api/test/integration/validateMessage.test.js index d71bef8b..0d44c401 100644 --- a/lib/api/test/integration/validateMessage.test.js +++ b/lib/api/test/integration/validateMessage.test.js @@ -90,15 +90,16 @@ describe('validateMessage', () => { }); describe('method', () => { + // See https://github.com/apiaryio/gavel.js/issues/158 it.skip('compares methods'); }); - describe.skip('headers', () => { - it('has no validator set', () => { - assert.propertyVal(result.headers, 'validator', null); + describe('headers', () => { + it('has "HeadersJsonExample" validator', () => { + assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample'); }); - it('has "application/vnd.apiary.http-headers+json" as real headers type', () => { + it('has "application/vnd.apiary.http-headers+json" as real type', () => { assert.propertyVal( result.headers, 'realType', @@ -106,30 +107,16 @@ describe('validateMessage', () => { ); }); - it('has no expected headers type', () => { - assert.propertyVal(result.headers, 'expectedType', null); + it('has "application/vnd.apiary.http-headers+json" expcted type', () => { + assert.propertyVal( + result.headers, + 'expectedType', + 'application/vnd.apiary.http-headers+json' + ); }); - describe('produces an error', () => { - it('exactly one error', () => { - assert.lengthOf(result.headers.results, 1); - }); - - it('has "error" severity', () => { - assert.propertyVal(result.headers.results[0], 'severity', 'error'); - }); - - it('has explanatory message', () => { - assert.propertyVal( - result.headers.results[0], - 'message', - `\ -No validator found for real data media type -"application/vnd.apiary.http-headers+json" -and expected data media type -"null".` - ); - }); + it('has no errors', () => { + assert.lengthOf(result.headers.results, 0); }); }); diff --git a/lib/api/test/unit/units/validateBody.test.js b/lib/api/test/unit/units/validateBody.test.js index 9b8b7518..6f6346b9 100644 --- a/lib/api/test/unit/units/validateBody.test.js +++ b/lib/api/test/unit/units/validateBody.test.js @@ -12,14 +12,14 @@ describe('validateBody', () => { ]; scenarios.forEach(({ name, value }) => { - it(`errors when given ${name}`, () => { + it(`throws when given ${name}`, () => { assert.throw(() => validateBody({ body: value }, { body: value })); }); }); }); describe('when given supported body type', () => { - describe('with explicit Content-Type header', () => { + describe('with explicit "Content-Type" header', () => { describe('application/json', () => { describe('with matching body type', () => { const res = validateBody( @@ -27,19 +27,21 @@ describe('validateBody', () => { body: '{ "foo": "bar" }', headers: { 'content-type': 'application/json' } }, - { body: '{ "foo": "bar" }' } + { + body: '{ "foo": "bar" }' + } ); - it('has application/json real type', () => { - assert.propertyVal(res, 'realType', 'application/json'); + it('has "JsonExample" validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); }); - it('has application/json expected type', () => { - assert.propertyVal(res, 'expectedType', 'application/json'); + it('has "application/json" real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); }); - it('has JsonExample validator', () => { - assert.propertyVal(res, 'validator', 'JsonExample'); + it('has "application/json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); }); it('has no errors', () => { @@ -56,16 +58,20 @@ describe('validateBody', () => { { body: '{ "foo": "bar" }' } ); - it('fallbacks to text/plain real type', () => { + it('has no validator', () => { + assert.propertyVal(res, 'validator', null); + }); + + it('fallbacks to "text/plain" real type', () => { assert.propertyVal(res, 'realType', 'text/plain'); }); - it('has application/json expected type', () => { + it('has "application/json" expected type', () => { assert.propertyVal(res, 'expectedType', 'application/json'); }); describe('produces content-type error', () => { - it('has proper severity', () => { + it('has "error" severity', () => { assert.propertyVal(res.results[0], 'severity', 'error'); }); @@ -76,10 +82,6 @@ describe('validateBody', () => { ); }); }); - - it('has no validator', () => { - assert.propertyVal(res, 'validator', null); - }); }); }); @@ -97,16 +99,16 @@ describe('validateBody', () => { } ); - it('has application/hal+json real type', () => { - assert.propertyVal(res, 'realType', 'application/hal+json'); + it('has "JsonExample" validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); }); - it('has application/json expected type', () => { - assert.propertyVal(res, 'expectedType', 'application/json'); + it('has "application/hal+json" real type', () => { + assert.propertyVal(res, 'realType', 'application/hal+json'); }); - it('has JsonExample validator', () => { - assert.propertyVal(res, 'validator', 'JsonExample'); + it('has "application/json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); }); it('has no errors', () => { @@ -127,11 +129,15 @@ describe('validateBody', () => { } ); - it('fallbacks to text/plain real type', () => { + it('has no validator', () => { + assert.propertyVal(res, 'validator', null); + }); + + it('fallbacks to "text/plain" real type', () => { assert.propertyVal(res, 'realType', 'text/plain'); }); - it('has text/plain expected type', () => { + it('has "text/plain" expected type', () => { assert.propertyVal(res, 'expectedType', 'text/plain'); }); @@ -139,6 +145,7 @@ describe('validateBody', () => { it('has "error" severity', () => { assert.propertyVal(res.results[0], 'severity', 'error'); }); + it('has explanatory message', () => { assert.match( res.results[0].message, @@ -146,31 +153,27 @@ describe('validateBody', () => { ); }); }); - - it('has no validator', () => { - assert.propertyVal(res, 'validator', null); - }); }); }); }); - describe('without explicit Content-Type header', () => { + describe('without explicit "Content-Type" header', () => { describe('text/plain', () => { describe('with matching bodies', () => { const res = validateBody({ body: 'foo' }, { body: 'foo' }); + it('has "TextDiff" validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); + }); + it('has text/plain real type', () => { assert.propertyVal(res, 'realType', 'text/plain'); }); - it('has text/plain expected type', () => { + it('has "text/plain" expected type', () => { assert.propertyVal(res, 'expectedType', 'text/plain'); }); - it('has TextDiff validator', () => { - assert.propertyVal(res, 'validator', 'TextDiff'); - }); - it('has no errors', () => { assert.lengthOf(res.results, 0); }); @@ -179,16 +182,16 @@ describe('validateBody', () => { describe('with non-matching bodies', () => { const res = validateBody({ body: 'foo ' }, { body: 'bar' }); - it('has text/plain real type', () => { - assert.propertyVal(res, 'realType', 'text/plain'); + it('has "TextDiff" validator', () => { + assert.propertyVal(res, 'validator', 'TextDiff'); }); - it('has text/plain expected type', () => { - assert.propertyVal(res, 'expectedType', 'text/plain'); + it('has "text/plain" real type', () => { + assert.propertyVal(res, 'realType', 'text/plain'); }); - it('has TextDiff validator', () => { - assert.propertyVal(res, 'validator', 'TextDiff'); + it('has "text/plain" expected type', () => { + assert.propertyVal(res, 'expectedType', 'text/plain'); }); describe('produces validation error', () => { @@ -219,16 +222,16 @@ describe('validateBody', () => { { body: '{ "foo": "bar" }' } ); - it('has application/json real type', () => { - assert.propertyVal(res, 'realType', 'application/json'); + it('has "JsonExample" validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); }); - it('has application/json expected type', () => { - assert.propertyVal(res, 'expectedType', 'application/json'); + it('has "application/json" real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); }); - it('has JsonExample validator', () => { - assert.propertyVal(res, 'validator', 'JsonExample'); + it('has "application/json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); }); it('has no errors', () => { @@ -242,16 +245,16 @@ describe('validateBody', () => { { body: '{ "bar": null }' } ); - it('has application/json real type', () => { - assert.propertyVal(res, 'realType', 'application/json'); + it('has "JsonExample" validator', () => { + assert.propertyVal(res, 'validator', 'JsonExample'); }); - it('has application/json expected type', () => { - assert.propertyVal(res, 'expectedType', 'application/json'); + it('has "application/json" real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); }); - it('has JsonExample validator', () => { - assert.propertyVal(res, 'validator', 'JsonExample'); + it('has "application/json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/json'); }); describe('produces validation errors', () => { diff --git a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js index edd400db..adc102e2 100644 --- a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js +++ b/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js @@ -4,45 +4,68 @@ const { getBodySchemaType } = require('../../../../units/validateBody'); describe('getBodySchemaType', () => { describe('when given non-string schema', () => { - // ... - }); + const values = [ + ['number', 123], + ['object', { foo: 'bar' }], + ['array', [1, 2, 3]], + ['null', null], + ['undefined', undefined] + ]; - describe('when given json schema', () => { - describe('that contains object', () => { - const res = getBodySchemaType('{ "foo": "bar" }'); + values.forEach(([name, value]) => { + describe(`which is ${name}`, () => { + const [error, mediaType] = getBodySchemaType(value); - it('returns no errors', () => { - assert.isNull(res[0]); - }); + it('returns "application/schema+json" media type', () => { + assert.deepEqual( + mediaType, + mediaTyper.parse('application/schema+json') + ); + }); - it('returns "application/schema+json" media type', () => { - assert.deepEqual(res[1], mediaTyper.parse('application/schema+json')); + it('has no errors', () => { + assert.isNull(error); + }); }); }); + }); - describe('that contains array', () => { - const res = getBodySchemaType('[1, 2, 3]'); + describe('when given JSON', () => { + const jsonSchemas = [ + ['object', '{ "foo": "bar" }'], + ['array', '[1, 2, 3]'] + ]; - it('returns no errors', () => { - assert.isNull(res[0]); - }); + jsonSchemas.forEach(([name, jsonSchema]) => { + describe(`that contains ${name}`, () => { + const [error, mediaType] = getBodySchemaType(jsonSchema); - it('returns "application/schema+json" media type', () => { - assert.deepEqual(res[1], mediaTyper.parse('application/schema+json')); + it('returns "application/schema+json" media type', () => { + assert.deepEqual( + mediaType, + mediaTyper.parse('application/schema+json') + ); + }); + + it('returns no errors', () => { + assert.isNull(error); + }); }); }); }); - describe('when given non-json schema', () => { - const res = getBodySchemaType('foo'); + describe('when given non-JSON string', () => { + const [error, mediaType] = getBodySchemaType('foo'); + + it('returns no media type', () => { + assert.isNull(mediaType); + }); it('returns parsing error', () => { assert.match( - res[0], + error, /^Can't validate: expected body JSON Schema is not a parseable JSON:/ ); }); - - it('returns no media type', () => [assert.isNull(res[1])]); }); }); diff --git a/lib/api/test/unit/units/validateBody/getBodyType.test.js b/lib/api/test/unit/units/validateBody/getBodyType.test.js index 4d58786a..d9ba8dd3 100644 --- a/lib/api/test/unit/units/validateBody/getBodyType.test.js +++ b/lib/api/test/unit/units/validateBody/getBodyType.test.js @@ -6,10 +6,10 @@ const jsonTypes = ['application/json', 'application/schema+json']; const nonJsonTypes = ['text/plain']; describe('getBodyType', () => { - describe('when given json-like content type', () => { + describe('when given JSON-like content type', () => { jsonTypes.forEach((jsonType) => { describe(`when given "${jsonType}" content type`, () => { - describe('and parsable json body', () => { + describe('and parsable JSON body', () => { const res = getBodyType('{ "foo": "bar" }', jsonType, 'real'); it('returns no errors', () => { @@ -21,7 +21,7 @@ describe('getBodyType', () => { }); }); - describe('and non-parsable json body', () => { + describe('and non-parsable JSON body', () => { const res = getBodyType('abc', jsonType, 'real'); it('returns parsing error', () => { @@ -46,7 +46,7 @@ describe('getBodyType', () => { }); }); - describe('when given non-json content type', () => { + describe('when given non-JSON content type', () => { nonJsonTypes.forEach((nonJsonType) => { describe(`when given "${nonJsonType}" content type`, () => { describe('and parsable json body', () => { @@ -61,7 +61,7 @@ describe('getBodyType', () => { }); }); - describe('and a non-json body', () => { + describe('and a non-JSON body', () => { const res = getBodyType('abc', nonJsonType, 'real'); it('returns no errors', () => { diff --git a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js index 9e0bfa57..16bf3d2e 100644 --- a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js +++ b/lib/api/test/unit/units/validateBody/getBodyValidator.test.js @@ -43,4 +43,24 @@ 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/lib/api/test/unit/units/validateBody/isJson.test.js b/lib/api/test/unit/units/validateBody/isJson.test.js index 38519801..54ec2008 100644 --- a/lib/api/test/unit/units/validateBody/isJson.test.js +++ b/lib/api/test/unit/units/validateBody/isJson.test.js @@ -4,17 +4,17 @@ const { isJson } = require('../../../../units/validateBody'); describe('isJson', () => { describe('returns true', () => { - it('when given valid json media type', () => { + it('when given valid JSON media type', () => { assert.isTrue(isJson(mediaTyper.parse('application/json'))); }); - it('when given json-compatible media type', () => { + it('when given JSON-compatible media type', () => { assert.isTrue(isJson(mediaTyper.parse('application/schema+json'))); }); }); describe('returns false', () => { - it('when given non-json media type', () => { + it('when given non-JSON media type', () => { assert.isFalse(isJson(mediaTyper.parse('text/plain'))); }); diff --git a/lib/api/test/unit/units/validateBody/isJsonSchema.test.js b/lib/api/test/unit/units/validateBody/isJsonSchema.test.js index a2a4bb15..09f8ff86 100644 --- a/lib/api/test/unit/units/validateBody/isJsonSchema.test.js +++ b/lib/api/test/unit/units/validateBody/isJsonSchema.test.js @@ -4,7 +4,7 @@ const { isJsonSchema } = require('../../../../units/validateBody'); describe('isJsonSchema', () => { describe('returns true', () => { - it('when given a json schema media type', () => { + it('when given a JSON schema media type', () => { assert.isTrue(isJsonSchema(mediaTyper.parse('application/schema+json'))); }); }); diff --git a/lib/api/test/unit/units/validateStatusCode.test.js b/lib/api/test/unit/units/validateStatusCode.test.js index e22cfbbe..2c9592ef 100644 --- a/lib/api/test/unit/units/validateStatusCode.test.js +++ b/lib/api/test/unit/units/validateStatusCode.test.js @@ -12,15 +12,15 @@ describe('validateStatusCode', () => { } ); - it('has proper validator', () => { + it('has "TextDiff" validator', () => { assert.propertyVal(res, 'validator', 'TextDiff'); }); - it('has proper expected type', () => { + it('has "text/vnd.apiary.status-code" expected type', () => { assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); }); - it('has proper real type', () => { + it('has "text/vnd.apiary.status-code" real type', () => { assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); }); @@ -39,25 +39,34 @@ describe('validateStatusCode', () => { } ); - it('has proper validator', () => { + it('has "TextDiff" validator', () => { assert.propertyVal(res, 'validator', 'TextDiff'); }); - it('has proper expected type', () => { + it('has "text/vnd.apiary.status-code" expected type', () => { assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); }); - it('has proper real type', () => { + it('has "text/vnd.apiary.status-code" real type', () => { assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); }); - it('has errors', () => { - assert.deepPropertyVal(res, 'results', [ - { - message: 'Real and expected data does not match.', - severity: 'error' - } - ]); + describe('produces error', () => { + it('exactly one error', () => { + assert.lengthOf(res.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.results[0], + 'message', + 'Real and expected data does not match.' + ); + }); }); }); }); From f42f615534d10073daeb27d7822c2160705bd74a Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Fri, 17 May 2019 16:17:17 +0200 Subject: [PATCH 34/38] test: adds JsonSchema test case for "validateBody" --- lib/api/test/unit/units/validateBody.test.js | 70 +++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/lib/api/test/unit/units/validateBody.test.js b/lib/api/test/unit/units/validateBody.test.js index 6f6346b9..2449439c 100644 --- a/lib/api/test/unit/units/validateBody.test.js +++ b/lib/api/test/unit/units/validateBody.test.js @@ -277,8 +277,74 @@ describe('validateBody', () => { }); }); - describe.skip('application/schema+json', () => { - // ... + describe('application/schema+json', () => { + describe('with matching bodies', () => { + const res = validateBody( + { body: '{ "foo": "bar" }' }, + { + bodySchema: { + required: ['foo'] + } + } + ); + + it('has "JsonSchema" validator', () => { + assert.propertyVal(res, 'validator', 'JsonSchema'); + }); + + it('has "application/json" real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); + }); + + it('has "application/schema+json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/schema+json'); + }); + + it('has no errors', () => { + assert.lengthOf(res.results, 0); + }); + }); + + describe('with non-matching bodies', () => { + const res = validateBody( + { body: '{ "oneTwoThree": "bar" }' }, + { + bodySchema: { + required: ['doe'] + } + } + ); + + it('has "JsonSchema" validator', () => { + assert.propertyVal(res, 'validator', 'JsonSchema'); + }); + + it('has "application/json" real type', () => { + assert.propertyVal(res, 'realType', 'application/json'); + }); + + it('has "application/schema+json" expected type', () => { + assert.propertyVal(res, 'expectedType', 'application/schema+json'); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(res.results, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(res.results[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + res.results[0], + 'message', + `At '/doe' Missing required property: doe` + ); + }); + }); + }); }); }); }); From 5589c2f1e6ad104478994bb08f6f041f8327eef0 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 20 May 2019 09:44:26 +0200 Subject: [PATCH 35/38] test: fixes typos in validation tests --- lib/api/test/integration/validateMessage.test.js | 2 +- lib/api/test/unit/units/validateHeaders.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/test/integration/validateMessage.test.js b/lib/api/test/integration/validateMessage.test.js index 0d44c401..8ee36dd6 100644 --- a/lib/api/test/integration/validateMessage.test.js +++ b/lib/api/test/integration/validateMessage.test.js @@ -107,7 +107,7 @@ describe('validateMessage', () => { ); }); - it('has "application/vnd.apiary.http-headers+json" expcted type', () => { + it('has "application/vnd.apiary.http-headers+json" expected type', () => { assert.propertyVal( result.headers, 'expectedType', diff --git a/lib/api/test/unit/units/validateHeaders.test.js b/lib/api/test/unit/units/validateHeaders.test.js index 804da20a..530a6884 100644 --- a/lib/api/test/unit/units/validateHeaders.test.js +++ b/lib/api/test/unit/units/validateHeaders.test.js @@ -145,7 +145,7 @@ describe('validateHeaders', () => { assert.propertyVal(res.results[0], 'severity', 'error'); }); - it('has exlpanatory message', () => { + it('has explanatory message', () => { assert.propertyVal( res.results[0], 'message', From a0e3229172e121f1b262cdd8988190c274defe67 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 20 May 2019 09:52:15 +0200 Subject: [PATCH 36/38] refactor: removes explicit normalization from "validateHeaders" --- lib/api/units/normalize/normalizeHeaders.js | 11 +++++++---- lib/api/units/validateHeaders.js | 20 +++----------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/lib/api/units/normalize/normalizeHeaders.js b/lib/api/units/normalize/normalizeHeaders.js index a663fec5..a6c2b1b9 100644 --- a/lib/api/units/normalize/normalizeHeaders.js +++ b/lib/api/units/normalize/normalizeHeaders.js @@ -13,16 +13,19 @@ const normalizeHeaders = (headers) => { } const headersType = typeof headers; + const isHeadersNull = headers == null; - if (headersType === null || headersType !== 'object') { + if (isHeadersNull || headersType !== 'object') { throw new Error( - `Can't validate: expected "headers" to be an Object, but got: ${headersType}.` + `Can't validate: expected "headers" to be an Object, but got: ${ + isHeadersNull ? 'null' : headersType + }.` ); } return Object.keys(headers).reduce( - (acc, name) => ({ - ...acc, + (normalizedHeaders, name) => ({ + ...normalizedHeaders, [name.toLowerCase()]: typeof headers[name] === 'string' ? normalizeStringValue(headers[name]) diff --git a/lib/api/units/validateHeaders.js b/lib/api/units/validateHeaders.js index a68b778b..27f530cb 100644 --- a/lib/api/units/validateHeaders.js +++ b/lib/api/units/validateHeaders.js @@ -2,18 +2,6 @@ const { HeadersJsonExample } = require('../../validators/headers-json-example'); const APIARY_JSON_HEADER_TYPE = 'application/vnd.apiary.http-headers+json'; -function normalizeHeaders(headers) { - return headers instanceof Object - ? Object.keys(headers).reduce( - (acc, headerName) => - Object.assign({}, acc, { - [headerName.toLowerCase()]: headers[headerName] - }), - {} - ) - : null; -} - function getHeadersType(headers) { return headers instanceof Object && !Array.isArray(headers) ? APIARY_JSON_HEADER_TYPE @@ -26,10 +14,8 @@ function getHeadersType(headers) { * @param {Object} expected */ function validateHeaders(real, expected) { - const realHeaders = normalizeHeaders(real.headers); - const expectedHeaders = normalizeHeaders(expected.headers); - const realType = getHeadersType(realHeaders); - const expectedType = getHeadersType(expectedHeaders); + const realType = getHeadersType(real.headers); + const expectedType = getHeadersType(expected.headers); const results = []; const hasJsonHeaders = @@ -37,7 +23,7 @@ function validateHeaders(real, expected) { expectedType === APIARY_JSON_HEADER_TYPE; const validator = hasJsonHeaders - ? new HeadersJsonExample(realHeaders, expectedHeaders) + ? new HeadersJsonExample(real.headers, expected.headers) : null; const rawData = validator && validator.validate(); From 98a9e6aab76d70eaa21795aba8d924a7de913fdc Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 20 May 2019 10:09:36 +0200 Subject: [PATCH 37/38] refactor: adds status code normalization to normalization layer --- .../normalize/normalizeStatusCode.test.js | 30 +++++++++++++++++ .../unit/units/validateStatusCode.test.js | 32 +++++++++---------- lib/api/units/normalize/index.js | 2 ++ .../units/normalize/normalizeStatusCode.js | 5 +++ lib/api/units/validateStatusCode.js | 9 +----- 5 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 lib/api/test/unit/units/normalize/normalizeStatusCode.test.js create mode 100644 lib/api/units/normalize/normalizeStatusCode.js diff --git a/lib/api/test/unit/units/normalize/normalizeStatusCode.test.js b/lib/api/test/unit/units/normalize/normalizeStatusCode.test.js new file mode 100644 index 00000000..6cac3908 --- /dev/null +++ b/lib/api/test/unit/units/normalize/normalizeStatusCode.test.js @@ -0,0 +1,30 @@ +const { assert } = require('chai'); +const { + normalizeStatusCode +} = require('../../../../units/normalize/normalizeStatusCode'); + +describe('normalizeStatusCode', () => { + describe('when given a string', () => { + const normalized = normalizeStatusCode(' 400 '); + + it('returns a string', () => { + assert.isString(normalized); + }); + + it('trims the value', () => { + assert.equal(normalized, '400'); + }); + }); + + describe('when given falsy value', () => { + const values = [null, undefined]; + + values.forEach((value) => { + const normalized = normalizeStatusCode(value); + + it(`returns empty string when given ${value}`, () => { + assert.equal(normalized, ''); + }); + }); + }); +}); diff --git a/lib/api/test/unit/units/validateStatusCode.test.js b/lib/api/test/unit/units/validateStatusCode.test.js index 2c9592ef..651fc121 100644 --- a/lib/api/test/unit/units/validateStatusCode.test.js +++ b/lib/api/test/unit/units/validateStatusCode.test.js @@ -3,66 +3,66 @@ const { validateStatusCode } = require('../../../units/validateStatusCode'); describe('validateStatusCode', () => { describe('given matching status codes', () => { - const res = validateStatusCode( + const result = validateStatusCode( { - statusCode: 200 + statusCode: '200' }, { - statusCode: 200 + statusCode: '200' } ); it('has "TextDiff" validator', () => { - assert.propertyVal(res, 'validator', 'TextDiff'); + assert.propertyVal(result, 'validator', 'TextDiff'); }); it('has "text/vnd.apiary.status-code" expected type', () => { - assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); + assert.propertyVal(result, 'expectedType', 'text/vnd.apiary.status-code'); }); it('has "text/vnd.apiary.status-code" real type', () => { - assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); + assert.propertyVal(result, 'realType', 'text/vnd.apiary.status-code'); }); it('has no errors', () => { - assert.deepPropertyVal(res, 'results', []); + assert.deepPropertyVal(result, 'results', []); }); }); describe('given non-matching status codes', () => { - const res = validateStatusCode( + const result = validateStatusCode( { - statusCode: 200 + statusCode: '200' }, { - statusCode: 400 + statusCode: '400' } ); it('has "TextDiff" validator', () => { - assert.propertyVal(res, 'validator', 'TextDiff'); + assert.propertyVal(result, 'validator', 'TextDiff'); }); it('has "text/vnd.apiary.status-code" expected type', () => { - assert.propertyVal(res, 'expectedType', 'text/vnd.apiary.status-code'); + assert.propertyVal(result, 'expectedType', 'text/vnd.apiary.status-code'); }); it('has "text/vnd.apiary.status-code" real type', () => { - assert.propertyVal(res, 'realType', 'text/vnd.apiary.status-code'); + assert.propertyVal(result, 'realType', 'text/vnd.apiary.status-code'); }); describe('produces error', () => { it('exactly one error', () => { - assert.lengthOf(res.results, 1); + assert.lengthOf(result.results, 1); }); it('has "error" severity', () => { - assert.propertyVal(res.results[0], 'severity', 'error'); + assert.propertyVal(result.results[0], 'severity', 'error'); }); it('has explanatory message', () => { assert.propertyVal( - res.results[0], + result.results[0], 'message', 'Real and expected data does not match.' ); diff --git a/lib/api/units/normalize/index.js b/lib/api/units/normalize/index.js index a1bc2f1c..b5c0a9d9 100644 --- a/lib/api/units/normalize/index.js +++ b/lib/api/units/normalize/index.js @@ -1,7 +1,9 @@ const evolve = require('../../utils/evolve'); +const { normalizeStatusCode } = require('./normalizeStatusCode'); const { normalizeHeaders } = require('./normalizeHeaders'); const normalize = evolve({ + statusCode: normalizeStatusCode, headers: normalizeHeaders }); diff --git a/lib/api/units/normalize/normalizeStatusCode.js b/lib/api/units/normalize/normalizeStatusCode.js new file mode 100644 index 00000000..4673856c --- /dev/null +++ b/lib/api/units/normalize/normalizeStatusCode.js @@ -0,0 +1,5 @@ +function normalizeStatusCode(value) { + return value == null ? '' : String(value).trim(); +} + +module.exports = { normalizeStatusCode }; diff --git a/lib/api/units/validateStatusCode.js b/lib/api/units/validateStatusCode.js index 8b14ae4e..e668a500 100644 --- a/lib/api/units/validateStatusCode.js +++ b/lib/api/units/validateStatusCode.js @@ -2,20 +2,13 @@ const { TextDiff } = require('../../validators/text-diff'); const APIARY_STATUS_CODE_TYPE = 'text/vnd.apiary.status-code'; -function normalizeStatusCode(statusCode) { - return String(statusCode).trim(); -} - /** * Validates given real and expected status codes. * @param {Object} real * @param {number} expected */ function validateStatusCode(real, expected) { - const validator = new TextDiff( - normalizeStatusCode(real.statusCode), - normalizeStatusCode(expected.statusCode) - ); + const validator = new TextDiff(real.statusCode, expected.statusCode); const rawData = validator.validate(); return { From 20ec98632e85d899f7230e3f4d4a1d45f6b8e09b Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Mon, 20 May 2019 10:20:57 +0200 Subject: [PATCH 38/38] feat: adds new validation algorithm, release preparations --- lib/gavel.js | 8 +++++--- .../test/integration/validateMessage.test.js | 0 .../unit/units/normalize/normalizeHeaders.test.js | 0 .../unit/units/normalize/normalizeStatusCode.test.js | 0 .../test/unit/units/validateBody.test.js | 0 .../unit/units/validateBody/getBodySchemaType.test.js | 0 .../test/unit/units/validateBody/getBodyType.test.js | 0 .../unit/units/validateBody/getBodyValidator.test.js | 0 .../test/unit/units/validateBody/isJson.test.js | 0 .../unit/units/validateBody/isJsonContentType.test.js | 0 .../test/unit/units/validateBody/isJsonSchema.test.js | 0 .../test/unit/units/validateHeaders.test.js | 0 .../test/unit/units/validateStatusCode.test.js | 0 lib/{api => next}/test/unit/utils/evolve.test.js | 0 lib/{api => next}/units/normalize/index.js | 0 lib/{api => next}/units/normalize/normalizeHeaders.js | 0 .../units/normalize/normalizeStatusCode.js | 0 lib/{api => next}/units/validateBody.js | 0 lib/{api => next}/units/validateHeaders.js | 0 lib/{api => next}/units/validateStatusCode.js | 0 lib/{api => next}/utils/evolve.js | 0 lib/{api => next}/validate.js | 8 ++++++++ lib/{api => next}/validateMessage.js | 0 lib/validate.js | 11 +++++++++-- package.json | 6 +++--- 25 files changed, 25 insertions(+), 8 deletions(-) rename lib/{api => next}/test/integration/validateMessage.test.js (100%) rename lib/{api => next}/test/unit/units/normalize/normalizeHeaders.test.js (100%) rename lib/{api => next}/test/unit/units/normalize/normalizeStatusCode.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/getBodySchemaType.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/getBodyType.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/getBodyValidator.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/isJson.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/isJsonContentType.test.js (100%) rename lib/{api => next}/test/unit/units/validateBody/isJsonSchema.test.js (100%) rename lib/{api => next}/test/unit/units/validateHeaders.test.js (100%) rename lib/{api => next}/test/unit/units/validateStatusCode.test.js (100%) rename lib/{api => next}/test/unit/utils/evolve.test.js (100%) rename lib/{api => next}/units/normalize/index.js (100%) rename lib/{api => next}/units/normalize/normalizeHeaders.js (100%) rename lib/{api => next}/units/normalize/normalizeStatusCode.js (100%) rename lib/{api => next}/units/validateBody.js (100%) rename lib/{api => next}/units/validateHeaders.js (100%) rename lib/{api => next}/units/validateStatusCode.js (100%) rename lib/{api => next}/utils/evolve.js (100%) rename lib/{api => next}/validate.js (67%) rename lib/{api => next}/validateMessage.js (100%) diff --git a/lib/gavel.js b/lib/gavel.js index b9b859f2..353cc79b 100644 --- a/lib/gavel.js +++ b/lib/gavel.js @@ -1,15 +1,17 @@ const { HttpRequest, ExpectedHttpRequest } = require('./model/http-request'); const { HttpResponse, ExpectedHttpResponse } = require('./model/http-response'); - const { isValid, isValidatable } = require('./validate'); -const validate = require('./api/validate'); +const validate = require('./next/validate'); module.exports = { + // next + validate, + + // legacy HttpRequest, HttpResponse, ExpectedHttpRequest, ExpectedHttpResponse, - validate, isValid, isValidatable }; diff --git a/lib/api/test/integration/validateMessage.test.js b/lib/next/test/integration/validateMessage.test.js similarity index 100% rename from lib/api/test/integration/validateMessage.test.js rename to lib/next/test/integration/validateMessage.test.js diff --git a/lib/api/test/unit/units/normalize/normalizeHeaders.test.js b/lib/next/test/unit/units/normalize/normalizeHeaders.test.js similarity index 100% rename from lib/api/test/unit/units/normalize/normalizeHeaders.test.js rename to lib/next/test/unit/units/normalize/normalizeHeaders.test.js diff --git a/lib/api/test/unit/units/normalize/normalizeStatusCode.test.js b/lib/next/test/unit/units/normalize/normalizeStatusCode.test.js similarity index 100% rename from lib/api/test/unit/units/normalize/normalizeStatusCode.test.js rename to lib/next/test/unit/units/normalize/normalizeStatusCode.test.js diff --git a/lib/api/test/unit/units/validateBody.test.js b/lib/next/test/unit/units/validateBody.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody.test.js rename to lib/next/test/unit/units/validateBody.test.js diff --git a/lib/api/test/unit/units/validateBody/getBodySchemaType.test.js b/lib/next/test/unit/units/validateBody/getBodySchemaType.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/getBodySchemaType.test.js rename to lib/next/test/unit/units/validateBody/getBodySchemaType.test.js diff --git a/lib/api/test/unit/units/validateBody/getBodyType.test.js b/lib/next/test/unit/units/validateBody/getBodyType.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/getBodyType.test.js rename to lib/next/test/unit/units/validateBody/getBodyType.test.js diff --git a/lib/api/test/unit/units/validateBody/getBodyValidator.test.js b/lib/next/test/unit/units/validateBody/getBodyValidator.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/getBodyValidator.test.js rename to lib/next/test/unit/units/validateBody/getBodyValidator.test.js diff --git a/lib/api/test/unit/units/validateBody/isJson.test.js b/lib/next/test/unit/units/validateBody/isJson.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/isJson.test.js rename to lib/next/test/unit/units/validateBody/isJson.test.js diff --git a/lib/api/test/unit/units/validateBody/isJsonContentType.test.js b/lib/next/test/unit/units/validateBody/isJsonContentType.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/isJsonContentType.test.js rename to lib/next/test/unit/units/validateBody/isJsonContentType.test.js diff --git a/lib/api/test/unit/units/validateBody/isJsonSchema.test.js b/lib/next/test/unit/units/validateBody/isJsonSchema.test.js similarity index 100% rename from lib/api/test/unit/units/validateBody/isJsonSchema.test.js rename to lib/next/test/unit/units/validateBody/isJsonSchema.test.js diff --git a/lib/api/test/unit/units/validateHeaders.test.js b/lib/next/test/unit/units/validateHeaders.test.js similarity index 100% rename from lib/api/test/unit/units/validateHeaders.test.js rename to lib/next/test/unit/units/validateHeaders.test.js diff --git a/lib/api/test/unit/units/validateStatusCode.test.js b/lib/next/test/unit/units/validateStatusCode.test.js similarity index 100% rename from lib/api/test/unit/units/validateStatusCode.test.js rename to lib/next/test/unit/units/validateStatusCode.test.js diff --git a/lib/api/test/unit/utils/evolve.test.js b/lib/next/test/unit/utils/evolve.test.js similarity index 100% rename from lib/api/test/unit/utils/evolve.test.js rename to lib/next/test/unit/utils/evolve.test.js diff --git a/lib/api/units/normalize/index.js b/lib/next/units/normalize/index.js similarity index 100% rename from lib/api/units/normalize/index.js rename to lib/next/units/normalize/index.js diff --git a/lib/api/units/normalize/normalizeHeaders.js b/lib/next/units/normalize/normalizeHeaders.js similarity index 100% rename from lib/api/units/normalize/normalizeHeaders.js rename to lib/next/units/normalize/normalizeHeaders.js diff --git a/lib/api/units/normalize/normalizeStatusCode.js b/lib/next/units/normalize/normalizeStatusCode.js similarity index 100% rename from lib/api/units/normalize/normalizeStatusCode.js rename to lib/next/units/normalize/normalizeStatusCode.js diff --git a/lib/api/units/validateBody.js b/lib/next/units/validateBody.js similarity index 100% rename from lib/api/units/validateBody.js rename to lib/next/units/validateBody.js diff --git a/lib/api/units/validateHeaders.js b/lib/next/units/validateHeaders.js similarity index 100% rename from lib/api/units/validateHeaders.js rename to lib/next/units/validateHeaders.js diff --git a/lib/api/units/validateStatusCode.js b/lib/next/units/validateStatusCode.js similarity index 100% rename from lib/api/units/validateStatusCode.js rename to lib/next/units/validateStatusCode.js diff --git a/lib/api/utils/evolve.js b/lib/next/utils/evolve.js similarity index 100% rename from lib/api/utils/evolve.js rename to lib/next/utils/evolve.js diff --git a/lib/api/validate.js b/lib/next/validate.js similarity index 67% rename from lib/api/validate.js rename to lib/next/validate.js index b149fa00..14a6859a 100644 --- a/lib/api/validate.js +++ b/lib/next/validate.js @@ -1,5 +1,13 @@ const { validateMessage } = require('./validateMessage'); +/** + * Validates the given HTTP messages pair and returns + * a legacy-compliant validation results. + * @param {Object} real + * @param {Object} expected + * @param {'request'|'response'} type + * @param {(error: Error, result: Object) => void} callback + */ function validate(real, expected, type, callback) { if (type !== 'request' && type !== 'response') { throw new Error( diff --git a/lib/api/validateMessage.js b/lib/next/validateMessage.js similarity index 100% rename from lib/api/validateMessage.js rename to lib/next/validateMessage.js diff --git a/lib/validate.js b/lib/validate.js index 184612fc..2c6ea453 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -19,15 +19,22 @@ function proxy(validatableObject, method, cb) { * @param {'request'|'response'} type */ function getValidatableObject(real, expected, type) { + let request; + let response; + switch (type) { case 'request': - const request = new HttpRequest(real); + request = new HttpRequest(real); request.expected = new ExpectedHttpRequest(expected); return request; case 'response': - const response = new HttpResponse(real); + response = new HttpResponse(real); response.expected = new ExpectedHttpResponse(expected); return response; + default: + throw new Error( + `Can't validate: expected HTTP message type to be "request" or "response", but got: ${type}.` + ); } } diff --git a/package.json b/package.json index 539e588c..99a36d3c 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,15 @@ }, "scripts": { "lint": "eslint lib/**/*.js test/**/*.js", - "test": "npm run test:new && npm run test:server && npm run test:browser && npm run test:features", - "test:new": "mocha \"lib/api/test/**/*.test.js\"", + "test": "npm run test:next && npm run test:server && npm run test:browser && npm run test:features", + "test:next": "mocha \"lib/next/test/**/*.test.js\"", "test:server": "mocha \"test/unit/**/*-test.js\"", "test:server:coverage": "nyc npm run test:server", "test:browser": "mochify \"test/unit/**/*.js\"", "test:features": "node scripts/cucumber.js", "coveralls": "nyc --reporter=text-lcov npm run test:server | coveralls", "ci:lint": "npm run lint", - "ci:test": "npm run coveralls && npm run test:new && npm run test:browser && npm run test:features", + "ci:test": "npm run coveralls && npm run test:next && npm run test:browser && npm run test:features", "ci:release": "semantic-release", "precommit": "lint-staged" },