From b575e9353733b04c5ad726f71ad0ec3ea5316183 Mon Sep 17 00:00:00 2001 From: artem-zakharchenko Date: Tue, 4 Jun 2019 14:50:50 +0300 Subject: [PATCH] feat: adds "method" field validation --- .../test/integration/validateMessage.test.js | 38 ++++++- .../units/normalize/normalizeMethod.test.js | 14 +++ .../test/unit/units/validateMethod.test.js | 104 ++++++++++++++++++ lib/next/units/coerce/index.js | 1 + lib/next/units/normalize/index.js | 2 + lib/next/units/normalize/normalizeMethod.js | 9 ++ lib/next/units/validateMethod.js | 25 +++++ lib/next/validateMessage.js | 5 + 8 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 lib/next/test/unit/units/normalize/normalizeMethod.test.js create mode 100644 lib/next/test/unit/units/validateMethod.test.js create mode 100644 lib/next/units/normalize/normalizeMethod.js create mode 100644 lib/next/units/validateMethod.js diff --git a/lib/next/test/integration/validateMessage.test.js b/lib/next/test/integration/validateMessage.test.js index daf5ab3a..9287e00a 100644 --- a/lib/next/test/integration/validateMessage.test.js +++ b/lib/next/test/integration/validateMessage.test.js @@ -41,7 +41,14 @@ describe('validateMessage', () => { }); it('contains all validatable keys', () => { - assert.hasAllKeys(result.field, ['headers', 'body']); + assert.hasAllKeys(result.field, ['method', 'headers', 'body']); + }); + + describe('method', () => { + validator(result.field.method, null); + expectedType(result.field.method, 'text/vnd.apiary.method'); + realType(result.field.method, 'text/vnd.apiary.method'); + noErrors(result.field.method); }); describe('headers', () => { @@ -90,12 +97,35 @@ describe('validateMessage', () => { }); it('contains all validatable keys', () => { - assert.hasAllKeys(result.field, ['headers', 'body']); + assert.hasAllKeys(result.field, ['method', 'headers', 'body']); }); describe('method', () => { - // See https://github.com/apiaryio/gavel.js/issues/158 - it.skip('compares methods'); + validator(result.field.method, null); + expectedType(result.field.method, 'text/vnd.apiary.method'); + realType(result.field.method, 'text/vnd.apiary.method'); + + describe('produces one error', () => { + it('exactly one error', () => { + assert.lengthOf(result.field.method.errors, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal( + result.field.method.errors[0], + 'severity', + 'error' + ); + }); + + it('has explanatory message', () => { + assert.propertyVal( + result.field.method.errors[0], + 'message', + 'Expected "method" field to equal "PUT", but got "POST".' + ); + }); + }); }); describe('headers', () => { diff --git a/lib/next/test/unit/units/normalize/normalizeMethod.test.js b/lib/next/test/unit/units/normalize/normalizeMethod.test.js new file mode 100644 index 00000000..408e2744 --- /dev/null +++ b/lib/next/test/unit/units/normalize/normalizeMethod.test.js @@ -0,0 +1,14 @@ +const { assert } = require('chai'); +const { + normalizeMethod +} = require('../../../../units/normalize/normalizeMethod'); + +describe('normalizeMethod', () => { + it('removes trailing spaces', () => { + assert.equal(normalizeMethod(' POST '), 'POST'); + }); + + it('converts to uppercase', () => { + assert.equal(normalizeMethod('pUt'), 'PUT'); + }); +}); diff --git a/lib/next/test/unit/units/validateMethod.test.js b/lib/next/test/unit/units/validateMethod.test.js new file mode 100644 index 00000000..31434556 --- /dev/null +++ b/lib/next/test/unit/units/validateMethod.test.js @@ -0,0 +1,104 @@ +const { assert } = require('chai'); +const { validateMethod } = require('../../../units/validateMethod'); + +describe('validateMethod', () => { + describe('given matching methods', () => { + const result = validateMethod({ method: 'GET' }, { method: 'GET' }); + + it('has "isValid" as "true"', () => { + assert.propertyVal(result, 'isValid', true); + }); + + it('has "null" validator', () => { + assert.isNull(result.validator); + }); + + it('has "text/vnd.apiary.method" real type', () => { + assert.propertyVal(result, 'realType', 'text/vnd.apiary.method'); + }); + + it('has "text/vnd.apiary.method" expected type', () => { + assert.propertyVal(result, 'expectedType', 'text/vnd.apiary.method'); + }); + + it('has no errors', () => { + assert.lengthOf(result.errors, 0); + }); + }); + + describe('given non-matching methods', () => { + const result = validateMethod({ method: 'GET' }, { method: 'POST' }); + + it('returns "isValid" as "false"', () => { + assert.propertyVal(result, 'isValid', false); + }); + + it('has "null" validator', () => { + assert.propertyVal(result, 'validator', null); + }); + + it('has "text/vnd.apiary.method" real type', () => { + assert.propertyVal(result, 'realType', 'text/vnd.apiary.method'); + }); + + it('has "text/vnd.apiary.method" expected type', () => { + assert.propertyVal(result, 'expectedType', 'text/vnd.apiary.method'); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(result.errors, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(result.errors[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + result.errors[0], + 'message', + 'Expected "method" field to equal "POST", but got "GET".' + ); + }); + }); + }); + + describe('given expected, but no real method', () => { + const result = validateMethod({ method: '' }, { method: 'PATCH' }); + + it('returns "isValid" as "false"', () => { + assert.propertyVal(result, 'isValid', false); + }); + + it('has "null" validator', () => { + assert.propertyVal(result, 'validator', null); + }); + + it('has "text/vnd.apiary.method" real type', () => { + assert.propertyVal(result, 'realType', 'text/vnd.apiary.method'); + }); + + it('has "text/vnd.apiary.method" expected type', () => { + assert.propertyVal(result, 'expectedType', 'text/vnd.apiary.method'); + }); + + describe('produces an error', () => { + it('exactly one error', () => { + assert.lengthOf(result.errors, 1); + }); + + it('has "error" severity', () => { + assert.propertyVal(result.errors[0], 'severity', 'error'); + }); + + it('has explanatory message', () => { + assert.propertyVal( + result.errors[0], + 'message', + 'Expected "method" field to equal "PATCH", but got "".' + ); + }); + }); + }); +}); diff --git a/lib/next/units/coerce/index.js b/lib/next/units/coerce/index.js index 8458089b..e781722e 100644 --- a/lib/next/units/coerce/index.js +++ b/lib/next/units/coerce/index.js @@ -2,6 +2,7 @@ const evolve = require('../../utils/evolve'); const { coerceHeaders } = require('./coerceHeaders'); const coercionMap = { + method: (method) => method || '', headers: coerceHeaders }; diff --git a/lib/next/units/normalize/index.js b/lib/next/units/normalize/index.js index b5c0a9d9..7d12c4a3 100644 --- a/lib/next/units/normalize/index.js +++ b/lib/next/units/normalize/index.js @@ -1,8 +1,10 @@ const evolve = require('../../utils/evolve'); +const { normalizeMethod } = require('./normalizeMethod'); const { normalizeStatusCode } = require('./normalizeStatusCode'); const { normalizeHeaders } = require('./normalizeHeaders'); const normalize = evolve({ + method: normalizeMethod, statusCode: normalizeStatusCode, headers: normalizeHeaders }); diff --git a/lib/next/units/normalize/normalizeMethod.js b/lib/next/units/normalize/normalizeMethod.js new file mode 100644 index 00000000..dd8890e0 --- /dev/null +++ b/lib/next/units/normalize/normalizeMethod.js @@ -0,0 +1,9 @@ +/** + * Normalizes given HTTP message method. + * @param {string} method + */ +const normalizeMethod = (method) => { + return method.trim().toUpperCase(); +}; + +module.exports = { normalizeMethod }; diff --git a/lib/next/units/validateMethod.js b/lib/next/units/validateMethod.js new file mode 100644 index 00000000..22c0863e --- /dev/null +++ b/lib/next/units/validateMethod.js @@ -0,0 +1,25 @@ +const APIARY_METHOD_TYPE = 'text/vnd.apiary.method'; + +function validateMethod(real, expected) { + const { method: realMethod } = real; + const { method: expectedMethod } = expected; + const errors = []; + const isValid = realMethod === expectedMethod; + + if (!isValid) { + errors.push({ + message: `Expected "method" field to equal "${expectedMethod}", but got "${realMethod}".`, + severity: 'error' + }); + } + + return { + isValid, + validator: null, + realType: APIARY_METHOD_TYPE, + expectedType: APIARY_METHOD_TYPE, + errors + }; +} + +module.exports = { validateMethod }; diff --git a/lib/next/validateMessage.js b/lib/next/validateMessage.js index 4db66e76..a22821d1 100644 --- a/lib/next/validateMessage.js +++ b/lib/next/validateMessage.js @@ -2,6 +2,7 @@ const isset = require('../utils/isset'); const { coerce, coerceWeak } = require('./units/coerce'); const { normalize } = require('./units/normalize'); const { isValid } = require('./units/isValid'); +const { validateMethod } = require('./units/validateMethod'); const { validateStatusCode } = require('./units/validateStatusCode'); const { validateHeaders } = require('./units/validateHeaders'); const { validateBody } = require('./units/validateBody'); @@ -22,6 +23,10 @@ function validateMessage(realMessage, expectedMessage) { // However, we want to use the same coercion logic for any coercion type. const expected = normalize(coerceWeak(expectedMessage)); + if (expected.method) { + results.field.method = validateMethod(real, expected); + } + if (expected.statusCode) { results.field.statusCode = validateStatusCode(real, expected); }