Skip to content

Commit

Permalink
Add wildcard support to response validators
Browse files Browse the repository at this point in the history
  • Loading branch information
jordandobrev committed Apr 12, 2020
1 parent 9c7edec commit 3ccbbad
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 31 deletions.
82 changes: 63 additions & 19 deletions src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,61 @@ export class ResponseValidator {
const status = statusCode;
if (status) {
const statusXX = status.toString()[0] + 'XX';
let svalidator;
if (status in validators) {
const svalidator = validators[status];
validator = svalidator[contentType];
if (!validator) validator = svalidator[Object.keys(svalidator)[0]]; // take first for backwards compatibility
svalidator = validators[status];
} else if (statusXX in validators) {
const svalidator = validators[statusXX];
validator = svalidator[contentType];
if (!validator) validator = svalidator[Object.keys(svalidator)[0]]; // take first for backwards compatibility
svalidator = validators[statusXX];
} else if (validators.default) {
const svalidator = validators.default;
validator = svalidator[contentType];
if (!validator) validator = svalidator[Object.keys(svalidator)[0]]; // take first for backwards compatibility
svalidator = validators.default;
} else {
throw validationError(
500,
path,
`no schema defined for status code '${status}' in the openapi spec`,
);
}

validator = svalidator[contentType];

if (!validator) { // wildcard support
for (const validatorContentType of Object.keys(svalidator).sort().reverse()) {
if (validatorContentType === '*/*') {
validator = svalidator[validatorContentType];
break;
}

if (RegExp(/^[a-z]+\/\*$/).test(validatorContentType)) { // wildcard of type application/*
const [type] = validatorContentType.split('/', 1);

if (new RegExp(`^${type}\/.+$`).test(contentType)) {
validator = svalidator[validatorContentType];
break;
}
}
}
}

if (!validator) validator = svalidator[Object.keys(svalidator)[0]]; // take first for backwards compatibility
}

if (!validator) {
console.warn('no validator found');
// assume valid
return;
}

if (!body) {
throw validationError(500, '.response', 'response body required.');
throw validationError(501, '.response', 'response body required.');
}

// CHECK If Content-Type is validatable
if (!this.canValidateContentType(contentType)) {
console.warn('Cannot validate content type', contentType);
// assume valid
return;
}

const valid = validator({
response: body,
});
Expand Down Expand Up @@ -167,13 +193,7 @@ export class ResponseValidator {
const types: string[] = [];
for (let contentType of Object.keys(response.content)) {
try {
const contentTypeParsed = contentTypeParser.parse(contentType);
const mediaTypeParsed = mediaTypeParser.parse(contentTypeParsed.type);

if (
mediaTypeParsed.subtype === 'json' ||
mediaTypeParsed.suffix === 'json'
) {
if (this.canValidateContentType(contentType)) {
if (
response.content[contentType] &&
response.content[contentType].schema
Expand All @@ -182,8 +202,13 @@ export class ResponseValidator {
}
}
} catch (e) {
// TODO remove this console log
console.error('***Skipping - need to handle wildcard:', e.message, '***');
// Handle wildcards
if (
response.content[contentType].schema &&
(contentType === '*/*' || new RegExp(/^[a-z]+\/\*$/).test(contentType))
) {
types.push(contentType);
}
}
}

Expand All @@ -207,6 +232,7 @@ export class ResponseValidator {
const schema = response.content[mediaTypeToValidate].schema;

responseSchemas[name] = {
...responseSchemas[name],
[mediaTypeToValidate]: {
// $schema: 'http://json-schema.org/schema#',
// $schema: "http://json-schema.org/draft-04/schema#",
Expand All @@ -224,11 +250,29 @@ export class ResponseValidator {
for (const [code, contentTypeSchemas] of Object.entries(responseSchemas)) {
for (const contentType of Object.keys(contentTypeSchemas)) {
const schema = contentTypeSchemas[contentType];

validators[code] = {
...validators[code],
[contentType]: this.ajv.compile(<object>schema),
};
}
}
return validators;
}

/**
* Checks if specific Content-Type is validatable
* @param contentType
* @returns boolean
* @throws error on invalid content type format
*/
private canValidateContentType(contentType: string): boolean {
const contentTypeParsed = contentTypeParser.parse(contentType);
const mediaTypeParsed = mediaTypeParser.parse(contentTypeParsed.type);

return (
mediaTypeParsed.subtype === 'json' ||
mediaTypeParsed.suffix === 'json'
);
}
}
2 changes: 1 addition & 1 deletion src/middlewares/parsers/body.parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class BodySchemaParser {
}

if (!content) {
for (const requestContentType of Object.keys(requestBody.content)) {
for (const requestContentType of Object.keys(requestBody.content).sort().reverse()) {
if (requestContentType === '*/*') {
content = requestBody.content[requestContentType];
break;
Expand Down
18 changes: 7 additions & 11 deletions test/request.bodies.ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as request from 'supertest';
import { createApp } from './common/app';
import * as packageJson from '../package.json';

describe.only(packageJson.name, () => {
describe(packageJson.name, () => {
let app = null;

before(async () => {
Expand All @@ -20,18 +20,12 @@ describe.only(packageJson.name, () => {
app => {
// Define new coercion routes
app.post(`${app.basePath}/request_bodies_ref`, (req, res) => {
if (req.header('accept') && req.header('accept').indexOf('text/plain') > -1) {
res.type('text').send(req.body);
} else if (req.header('accept') && req.header('accept').indexOf('text/html') > -1) {
res.type('html').send(req.body);
} else if (req.header('accept') && req.header('accept').indexOf('application/hal+json') > -1) {
res.type('application/hal+json').send(req.body);
} else if (req.header('accept') && req.header('accept').indexOf('application/vnd.api+json') > -1) {
res.type(req.header('accept')).send(req.body);
} else if (req.query.bad_body) {
if (req.query.bad_body) {
const r = req.body;
r.unexpected_prop = 'bad';
res.json(r);
} else if (req.header('accept')) {
res.type(req.header('accept')).send(req.body);
} else {
res.json(req.body);
}
Expand Down Expand Up @@ -67,6 +61,7 @@ describe.only(packageJson.name, () => {
.send(stringData)
.expect(200)
.then(r => {
expect(r.get('content-type')).to.contain('text/html')
expect(r.text).equals(stringData);
});
});
Expand Down Expand Up @@ -98,7 +93,8 @@ describe.only(packageJson.name, () => {
.expect(200)
.then(r => {
const { body } = r;
expect(r.get('content-type')).to.contain('application/vnd.api+json; type=two')
expect(r.get('content-type')).to.contain('application/vnd.api+json')
expect(r.get('content-type')).to.contain(' type=two')
expect(body).to.have.property('testPropertyTwo');
});
});
Expand Down

0 comments on commit 3ccbbad

Please sign in to comment.