diff --git a/package.json b/package.json index a546f42e..148cb833 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "compile": "rm -rf dist/ && tsc", "compile:windows": "rmdir dist /s /q & tsc", "test": "TS_NODE_FILES=true mocha -r source-map-support/register -r ts-node/register --files --recursive test/**/*.spec.ts", + "test:debug": "TS_NODE_FILES=true mocha -r source-map-support/register -r ts-node/register --inspect-brk --files --recursive test/**/*.spec.ts", "test:windows": "set TS_NODE_FILES=true & mocha -r source-map-support/register -r ts-node/register --files --recursive test/**/*.spec.ts", "test:coverage": "TS_NODE_FILES=true nyc mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts", "test:coverage:windows": "set TS_NODE_FILES=true & nyc mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts", "coveralls": "cat coverage/lcov.info | coveralls -v", "codacy": "cat coverage/lcov.info | codacy-coverage", "test:reset": "rm -rf node_modules && npm i && npm run compile && npm t" + }, "repository": { "url": "https://github.com/cdimascio/express-openapi-validator" diff --git a/src/framework/ajv/index.ts b/src/framework/ajv/index.ts index 19fd262b..9915437d 100644 --- a/src/framework/ajv/index.ts +++ b/src/framework/ajv/index.ts @@ -39,7 +39,7 @@ function createAjv( ajv.removeKeyword('readOnly'); ajv.addKeyword('readOnly', { modifying: true, - compile: sch => { + compile: (sch) => { if (sch) { return function validate(data, path, obj, propName) { const isValid = !(sch === true && data != null); @@ -65,7 +65,7 @@ function createAjv( ajv.removeKeyword('writeOnly'); ajv.addKeyword('writeOnly', { modifying: true, - compile: sch => { + compile: (sch) => { if (sch) { return function validate(data, path, obj, propName) { const isValid = !(sch === true && data != null); diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index a67a241a..82b3ee3a 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -229,7 +229,14 @@ export class ResponseValidator { }; const responseSchemas = {}; - for (const [name, response] of Object.entries(responses)) { + for (const [name, resp] of Object.entries(responses)) { + let tmpResponse = resp; + if (tmpResponse.$ref) { + // resolve response + const id = tmpResponse.$ref.replace(/^.+\//i, ''); + tmpResponse = this.spec.components?.responses?.[id]; + } + const response = tmpResponse; const types = validationTypes(response); for (const mediaTypeToValidate of types) { diff --git a/test/resources/response.validation.yaml b/test/resources/response.validation.yaml index b60d477b..1d0d0734 100644 --- a/test/resources/response.validation.yaml +++ b/test/resources/response.validation.yaml @@ -14,12 +14,18 @@ info: servers: - url: /v1 paths: + /ref_response_body: + get: + operationId: refResponseBody + responses: + '200': + $ref: '#/components/responses/ResponseBody' /empty_response: description: get empty response summary: get empty response get: description: get empty response - operationId: get empty resp + operationId: getEmptyResponse parameters: - name: mode in: query @@ -146,8 +152,8 @@ paths: post: description: Get todos operationId: getTodos - security: - - bearerAuth: [] + # security: + # - bearerAuth: [] responses: '200': description: no additional props @@ -157,6 +163,14 @@ paths: $ref: '#/components/schemas/NoAdditionalProps' components: + responses: + ResponseBody: + description: Sample response + content: + application/json: + schema: + $ref: '#/components/schemas/Human' + schemas: Object: type: object diff --git a/test/response.validation.spec.ts b/test/response.validation.spec.ts index 4e83b2e2..02d7de53 100644 --- a/test/response.validation.spec.ts +++ b/test/response.validation.spec.ts @@ -18,7 +18,10 @@ describe(packageJson.name, () => { validateResponses: true, }, 3005, - app => { + (app) => { + app.get(`${app.basePath}/ref_response_body`, (req, res) => { + return res.json({ id: 213, name: 'name', kids: [] }); + }); app.get(`${app.basePath}/empty_response`, (req, res) => { return res.end(); }); @@ -61,6 +64,7 @@ describe(packageJson.name, () => { res.json(req.body); }); app.use((err, req, res, next) => { + console.error(err); res.status(err.status ?? 500).json({ message: err.message, code: err.status ?? 500, @@ -75,27 +79,31 @@ describe(packageJson.name, () => { app.server.close(); }); + it('should return 200 on valid responses 200 $ref', async () => + request(app) + .get(`${app.basePath}/ref_response_body`) + .expect(200) + .then((r: any) => { + expect(r.body.id).to.be.a('number').that.equals(213); + })); + it('should fail if response field has a value of incorrect type', async () => request(app) .get(`${app.basePath}/pets?mode=bad_type`) .expect(500) .then((r: any) => { expect(r.body.message).to.contain('should be integer'); - expect(r.body) - .to.have.property('code') - .that.equals(500); + expect(r.body).to.have.property('code').that.equals(500); })); - // TODO add test for allOf - when allOf is used an array value passes when object is expected + // TODO add test for allOf - when allOf is used an array value passes when object is expected it('should fail if response is array when expecting object', async () => request(app) .get(`${app.basePath}/object`) .expect(500) .then((r: any) => { expect(r.body.message).to.contain('should be object'); - expect(r.body) - .to.have.property('code') - .that.equals(500); + expect(r.body).to.have.property('code').that.equals(500); })); // TODO fix me - fails for response and request validation what allOf is in use @@ -106,9 +114,7 @@ describe(packageJson.name, () => { .expect(400) .then((r: any) => { expect(r.body.message).to.contain('should be object'); - expect(r.body) - .to.have.property('code') - .that.equals(500); + expect(r.body).to.have.property('code').that.equals(500); })); it('should fail if response is response is empty object', async () => @@ -118,9 +124,7 @@ describe(packageJson.name, () => { .then((r: any) => { console.log(r.body); expect(r.body.message).to.contain('should be array'); - expect(r.body) - .to.have.property('code') - .that.equals(500); + expect(r.body).to.have.property('code').that.equals(500); })); it('should fail if response is response is empty', async () => @@ -129,15 +133,11 @@ describe(packageJson.name, () => { .expect(500) .then((r: any) => { expect(r.body.message).to.contain('body required'); - expect(r.body) - .to.have.property('code') - .that.equals(500); + expect(r.body).to.have.property('code').that.equals(500); })); it('should return 200 for endpoints that return empty response', async () => - request(app) - .get(`${app.basePath}/empty_response`) - .expect(200)); + request(app).get(`${app.basePath}/empty_response`).expect(200)); it('should fail if additional properties are provided when set false', async () => request(app) @@ -184,9 +184,7 @@ describe(packageJson.name, () => { .get(`${app.basePath}/pets?mode=check_null`) .expect(200) .then((r: any) => { - expect(r.body) - .is.an('array') - .with.length(3); + expect(r.body).is.an('array').with.length(3); expect(r.body[0].bought_at).equal(null); expect(r.body[1].bought_at).equal(today.toISOString()); expect(r.body[2].bought_at).to.be.undefined; @@ -197,8 +195,6 @@ describe(packageJson.name, () => { .get(`${app.basePath}/users`) .expect(200) .then((r: any) => { - expect(r.body) - .is.an('array') - .with.length(3); + expect(r.body).is.an('array').with.length(3); })); });