diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index 6c2b85b2..520f8d0b 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -106,18 +106,13 @@ 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, @@ -125,6 +120,28 @@ export class ResponseValidator { `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) { @@ -132,9 +149,18 @@ export class ResponseValidator { // 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, }); @@ -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 @@ -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); + } } } @@ -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#", @@ -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(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' + ); + } } diff --git a/src/middlewares/parsers/body.parse.ts b/src/middlewares/parsers/body.parse.ts index 1b6508f8..78915503 100644 --- a/src/middlewares/parsers/body.parse.ts +++ b/src/middlewares/parsers/body.parse.ts @@ -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; diff --git a/test/request.bodies.ref.spec.ts b/test/request.bodies.ref.spec.ts index 68a6f924..9e9d7367 100644 --- a/test/request.bodies.ref.spec.ts +++ b/test/request.bodies.ref.spec.ts @@ -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 () => { @@ -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); } @@ -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); }); }); @@ -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'); }); });