From 91fb31c86917ae03873821670f34d98184ac07fa Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Tue, 26 Nov 2019 21:57:35 -0500 Subject: [PATCH] Doesen't support readOnly + required combination #145 --- package-lock.json | 20 +++++------ package.json | 4 +-- src/middlewares/openapi.request.validator.ts | 33 +++++++++++++++++ test/read.only.spec.ts | 38 ++++++++++++++++++++ test/resources/read.only.yaml | 38 +++++++++++++++++--- 5 files changed, 117 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8a6c8ee8..1b131821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "2.17.0", + "version": "2.17.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1549,9 +1549,9 @@ "dev": true }, "handlebars": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.2.tgz", - "integrity": "sha512-29Zxv/cynYB7mkT1rVWQnV7mGX6v7H/miQ6dbEpYTKq5eJBN7PsRB+ViYJlcT6JINTSu4dVB9kOqEun78h6Exg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -3715,15 +3715,15 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, "uglify-js": { - "version": "3.6.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", - "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.0.tgz", + "integrity": "sha512-PC/ee458NEMITe1OufAjal65i6lB58R1HWMRcxwvdz1UopW0DYqlRL3xdu3IcTvTXsB02CRHykidkTRL+A3hQA==", "dev": true, "optional": true, "requires": { diff --git a/package.json b/package.json index 54cdeac2..a766dab9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "2.17.0", + "version": "2.17.1", "description": "Automatically validate API requests and responses with OpenAPI 3 and Express.", "main": "dist/index.js", "scripts": { @@ -60,6 +60,6 @@ "supertest": "^4.0.2", "ts-node": "^8.3.0", "tsc": "^1.20150623.0", - "typescript": "^3.6.4" + "typescript": "^3.7.2" } } diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index 36c9cafb..543a3eb2 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -233,6 +233,7 @@ export class RequestValidator { content = requestBody.content[type]; if (content) break; } + if (!content) { const msg = contentType.contentType === 'not_provided' @@ -240,6 +241,38 @@ export class RequestValidator { : `unsupported media type ${contentType.contentType}`; throw validationError(415, path, msg); } + + let bodyContentSchema = + requestBody.content[contentType.contentType] && + requestBody.content[contentType.contentType].schema; + if (bodyContentSchema && '$ref' in bodyContentSchema) { + const objectSchema = this.ajv.getSchema(bodyContentSchema.$ref); + if ( + objectSchema && + objectSchema.schema && + (objectSchema.schema).properties + ) { + // handle readonly / required request body refs + // don't need to copy schema if validator gets its own copy of the api spec + // currently all middlware i.e. req and res validators share the spec + const schema = { ...(objectSchema).schema }; + Object.keys(schema.properties).forEach(prop => { + const propertyValue = schema.properties[prop]; + + const required = schema.required; + if (propertyValue.readOnly && required) { + const index = required.indexOf(prop); + if (index > -1) { + schema.required = required + .slice(0, index) + .concat(required.slice(index + 1)); + } + } + }); + return schema; + } + } + return content.schema || {}; } return {}; diff --git a/test/read.only.spec.ts b/test/read.only.spec.ts index 5bd76537..d7e0120d 100644 --- a/test/read.only.spec.ts +++ b/test/read.only.spec.ts @@ -28,6 +28,12 @@ describe(packageJson.name, () => { .post(`${app.basePath}/products/inlined`, (req, res) => res.json(req.body), ) + .post(`${app.basePath}/user`, (req, res) => + res.json({ + ...req.body, + ...(req.query.include_id ? { id: 'test_id' } : {}), + }), + ) .post(`${app.basePath}/products/nested`, (req, res) => { const body = req.body; body.id = 'test'; @@ -130,4 +136,36 @@ describe(packageJson.name, () => { // id is a readonly property and should not be allowed in the request expect(body.message).to.contain('request.body.reviews[0].id'); })); + + it('should pass validation if required read only properties to be missing from request', async () => + request(app) + .post(`${app.basePath}/user`) + .set('content-type', 'application/json') + .query({ + include_id: true, + }) + .send({ + username: 'test', + }) + .expect(200) + .then(r => { + expect(r.body) + .to.be.an('object') + .with.property('id'); + expect(r.body).to.have.property('username'); + })); + + it('should fail validation if required read only properties is missing from the response', async () => + request(app) + .post(`${app.basePath}/user`) + .set('content-type', 'application/json') + .send({ + username: 'test', + }) + .expect(500) + .then(r => { + expect(r.body.errors[0]) + .to.have.property('message') + .equals("should have required property 'id'"); + })); }); diff --git a/test/resources/read.only.yaml b/test/resources/read.only.yaml index 9ea1fffe..a9dd15fa 100644 --- a/test/resources/read.only.yaml +++ b/test/resources/read.only.yaml @@ -8,9 +8,26 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - - url: http://petstore.swagger.io/v1 + - url: /v1 paths: + /user: + post: + description: get user + operationId: getUser + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '200': + description: user response + content: + application/json: + schema: + $ref: '#/components/schemas/User' /products: get: description: get products @@ -65,7 +82,7 @@ paths: format: date-time readOnly: true responses: - '200': + '200': description: pet response content: application/json: @@ -90,7 +107,6 @@ paths: schema: $ref: '#/components/schemas/ProductNested' - components: schemas: Product: @@ -136,4 +152,18 @@ components: readOnly: true rating: type: integer - + + User: + description: default + type: object + required: + - id + - username + properties: + id: + type: string + readOnly: true + username: + type: string + name: + type: string