Skip to content

Commit

Permalink
feat: coerces missing "body" of the actual HTTP message
Browse files Browse the repository at this point in the history
  • Loading branch information
artem-zakharchenko committed Jun 19, 2019
1 parent 0138366 commit 20aeca8
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 56 deletions.
3 changes: 2 additions & 1 deletion lib/units/coerce/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const otherwise = require('../../utils/otherwise');
const coercionMap = {
method: otherwise(''),
uri: otherwise(''),
headers: otherwise({})
headers: otherwise({}),
body: otherwise('')
};

// Coercion is strict by default, meaning it would populate
Expand Down
33 changes: 24 additions & 9 deletions lib/units/validateBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,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${
exception.message
}`;
const error = `Can't validate: expected body JSON Schema is not a parseable JSON:\n${exception.message}`;

return [error, null];
}
Expand Down Expand Up @@ -157,10 +155,14 @@ function getBodyValidator(realType, expectedType) {
*/
function validateBody(expected, real) {
const errors = [];
const bodyType = typeof real.body;
const realBodyType = typeof real.body;
const hasEmptyRealBody = real.body === '';

if (bodyType !== 'string') {
throw new Error(`Expected HTTP body to be a String, but got: ${bodyType}`);
// Throw when user input for real body is not a string.
if (realBodyType !== 'string') {
throw new Error(
`Expected HTTP body to be a string, but got: ${realBodyType}`
);
}

const [realTypeError, realType] = getBodyType(
Expand Down Expand Up @@ -197,9 +199,22 @@ function validateBody(expected, real) {
: getBodyValidator(realType, expectedType);

if (validatorError) {
errors.push({
message: validatorError
});
// In case determined media types mismtach, check if the real is not missing.
// Keep in mind the following scenarios:
// 1. Expcected '', and got '' (TextDiff/TextDiff, valid)
// 2. Expected {...}, but got '' (Json/TextDiff, invalid, produces "missing real body" error)
// 3. Expected {...}, but git "foo" (Json/TextDiff, invalid, produces types mismatch error).
if (expected.body !== '' && hasEmptyRealBody) {
errors.push({
message: `Expected "body" of "${mediaTyper.format(
expectedType
)}" media type, but actual HTTP message had no body.`
});
} else {
errors.push({
message: validatorError
});
}
}

const usesJsonSchema = ValidatorClass && ValidatorClass.name === 'JsonSchema';
Expand Down
103 changes: 63 additions & 40 deletions test/integration/validate.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const { assert } = require('chai');
const { validate } = require('../../lib/validate');

const isValid = (result, expectedValid = true) => {
it(`sets "isValid" to ${JSON.stringify(expectedValid)}`, () => {
assert.propertyVal(result, 'isValid', expectedValid);
});
};

const validator = (obj, expected) => {
it(`has "${expected}" validator`, () => {
assert.propertyVal(obj, 'validator', expected);
Expand Down Expand Up @@ -32,26 +38,22 @@ describe('validate', () => {
};
const result = validate(request, request);

it('returns validation result object', () => {
assert.isObject(result);
});

it('has "isValid" set to true', () => {
assert.propertyVal(result, 'isValid', true);
});
isValid(result);

it('contains all validatable keys', () => {
assert.hasAllKeys(result.fields, ['method', 'headers', 'body']);
});

describe('method', () => {
isValid(result.fields.method);
validator(result.fields.method, null);
expectedType(result.fields.method, 'text/vnd.apiary.method');
realType(result.fields.method, 'text/vnd.apiary.method');
noErrors(result.fields.method);
});

describe('headers', () => {
isValid(result.fields.headers);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand All @@ -65,6 +67,7 @@ describe('validate', () => {
});

describe('body', () => {
isValid(result.fields.body);
validator(result.fields.body, 'JsonExample');
expectedType(result.fields.body, 'application/json');
realType(result.fields.body, 'application/json');
Expand All @@ -88,19 +91,14 @@ describe('validate', () => {
}
);

it('returns validation result object', () => {
assert.isObject(result);
});

it('has "isValid" set to false', () => {
assert.propertyVal(result, 'isValid', false);
});
isValid(result, false);

it('contains all validatable keys', () => {
assert.hasAllKeys(result.fields, ['method', 'headers', 'body']);
});

describe('method', () => {
isValid(result.fields.method, false);
validator(result.fields.method, null);
expectedType(result.fields.method, 'text/vnd.apiary.method');
realType(result.fields.method, 'text/vnd.apiary.method');
Expand All @@ -121,6 +119,7 @@ describe('validate', () => {
});

describe('headers', () => {
isValid(result.fields.headers);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand All @@ -134,6 +133,7 @@ describe('validate', () => {
});

describe('body', () => {
isValid(result.fields.body, false);
validator(result.fields.body, 'JsonExample');
expectedType(result.fields.body, 'application/json');
realType(result.fields.body, 'application/json');
Expand Down Expand Up @@ -177,13 +177,15 @@ describe('validate', () => {
});

describe('statusCode', () => {
isValid(result.fields.statusCode);
validator(result.fields.statusCode, 'TextDiff');
expectedType(result.fields.statusCode, 'text/vnd.apiary.status-code');
realType(result.fields.statusCode, 'text/vnd.apiary.status-code');
noErrors(result.fields.statusCode);
});

describe('headers', () => {
isValid(result.fields.headers);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand All @@ -197,6 +199,7 @@ describe('validate', () => {
});

describe('body', () => {
isValid(result.fields.body);
validator(result.fields.body, 'JsonExample');
expectedType(result.fields.body, 'application/json');
realType(result.fields.body, 'application/json');
Expand All @@ -219,19 +222,14 @@ describe('validate', () => {
};
const result = validate(expectedResponse, realResponse);

it('returns validation result object', () => {
assert.isObject(result);
});

it('has "isValid" as false', () => {
assert.propertyVal(result, 'isValid', false);
});
isValid(result, false);

it('contains all validatable keys', () => {
assert.hasAllKeys(result.fields, ['statusCode', 'headers']);
});

describe('statusCode', () => {
isValid(result.fields.statusCode, false);
validator(result.fields.statusCode, 'TextDiff');
expectedType(result.fields.statusCode, 'text/vnd.apiary.status-code');
realType(result.fields.statusCode, 'text/vnd.apiary.status-code');
Expand All @@ -252,6 +250,7 @@ describe('validate', () => {
});

describe('headers', () => {
isValid(result.fields.headers, false);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand Down Expand Up @@ -291,26 +290,22 @@ describe('validate', () => {
}
);

it('returns validation result object', () => {
assert.isObject(result);
});

it('has "isValid" as false', () => {
assert.propertyVal(result, 'isValid', false);
});
isValid(result, false);

it('contains all validatable keys', () => {
assert.hasAllKeys(result.fields, ['statusCode', 'headers']);
});

describe('statusCode', () => {
isValid(result.fields.statusCode);
validator(result.fields.statusCode, 'TextDiff');
expectedType(result.fields.statusCode, 'text/vnd.apiary.status-code');
realType(result.fields.statusCode, 'text/vnd.apiary.status-code');
noErrors(result.fields.statusCode);
});

describe('headers', () => {
isValid(result.fields.headers, false);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand Down Expand Up @@ -348,41 +343,46 @@ describe('validate', () => {
describe('always validates expected properties', () => {
const result = validate(
{
method: 'POST',
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: '{ "foo": "bar" }'
},
{
body: 'doe'
method: 'PUT'
}
);

it('has "isValid" as false', () => {
assert.propertyVal(result, 'isValid', false);
});
isValid(result, false);

it('contains all validatable keys', () => {
assert.hasAllKeys(result.fields, ['statusCode', 'headers', 'body']);
assert.hasAllKeys(result.fields, [
'method',
'statusCode',
'headers',
'body'
]);
});

describe('for properties present in both expected and real', () => {
describe('body', () => {
validator(result.fields.body, null);
expectedType(result.fields.body, 'application/json');
realType(result.fields.body, 'text/plain');
describe('method', () => {
isValid(result.fields.method, false);
validator(result.fields.method, null);
expectedType(result.fields.method, 'text/vnd.apiary.method');
realType(result.fields.method, 'text/vnd.apiary.method');

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(result.fields.body.errors, 1);
assert.lengthOf(result.fields.method.errors, 1);
});

it('has explanatory message', () => {
assert.propertyVal(
result.fields.body.errors[0],
result.fields.method.errors[0],
'message',
`Can't validate real media type 'text/plain' against expected media type 'application/json'.`
'Expected "method" field to equal "POST", but got "PUT".'
);
});
});
Expand All @@ -391,6 +391,7 @@ describe('validate', () => {

describe('for properties present in expected, but not in real', () => {
describe('statusCode', () => {
isValid(result.fields.statusCode, false);
validator(result.fields.statusCode, 'TextDiff');
expectedType(result.fields.statusCode, 'text/vnd.apiary.status-code');
realType(result.fields.statusCode, 'text/vnd.apiary.status-code');
Expand All @@ -411,6 +412,7 @@ describe('validate', () => {
});

describe('headers', () => {
isValid(result.fields.headers, false);
validator(result.fields.headers, 'HeadersJsonExample');
expectedType(
result.fields.headers,
Expand All @@ -435,6 +437,27 @@ describe('validate', () => {
});
});
});

describe('body', () => {
isValid(result.fields.body, false);
validator(result.fields.body, null);
expectedType(result.fields.body, 'application/json');
realType(result.fields.body, 'text/plain');

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(result.fields.body.errors, 1);
});

it('has explanatory message', () => {
assert.propertyVal(
result.fields.body.errors[0],
'message',
'Expected "body" of "application/json" media type, but actual HTTP message had no body.'
);
});
});
});
});
});
});
16 changes: 10 additions & 6 deletions test/unit/units/validateBody.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ describe('validateBody', () => {
});

describe('with non-matching bodies', () => {
const res = validateBody(
const result = validateBody(
{
bodySchema: {
required: ['doe']
Expand All @@ -367,25 +367,29 @@ describe('validateBody', () => {
);

it('has "JsonSchema" validator', () => {
assert.propertyVal(res, 'validator', 'JsonSchema');
assert.propertyVal(result, 'validator', 'JsonSchema');
});

it('has "application/json" real type', () => {
assert.propertyVal(res, 'realType', 'application/json');
assert.propertyVal(result, 'realType', 'application/json');
});

it('has "application/schema+json" expected type', () => {
assert.propertyVal(res, 'expectedType', 'application/schema+json');
assert.propertyVal(
result,
'expectedType',
'application/schema+json'
);
});

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(res.errors, 1);
assert.lengthOf(result.errors, 1);
});

it('has explanatory message', () => {
assert.propertyVal(
res.errors[0],
result.errors[0],
'message',
`At '/doe' Missing required property: doe`
);
Expand Down

0 comments on commit 20aeca8

Please sign in to comment.