From cf58b30f755d47522b5f34faa021fa2f4d1fcc27 Mon Sep 17 00:00:00 2001 From: Richard Waller Date: Sun, 31 May 2020 15:48:23 +0100 Subject: [PATCH] test: increase jest-openapi test coverage to 100% (#91) * chore: update bug recreation template * test: tidy existing tests * test: sync tests for chai and jest plugins * tidy: sync chai and jest plugins * test: fix jest config --- .github/ISSUE_TEMPLATE/bug_report.md | 7 + CONTRIBUTING.md | 5 +- .../valid/bugRecreationTemplate/openapi.yml | 18 +- .../lib/classes/RequestPromiseResponse.js | 1 - .../differentRequestModules.test.js | 50 ++- .../satisfyApiSpec/satisfyApiSpec.test.js | 3 +- .../test/bug-recreation-template.test.js | 51 --- .../test/bugRecreationTemplate.test.js | 34 ++ packages/jest-openapi/__test__/.eslintrc.yml | 6 +- .../__test__/bugRecreationTemplate.test.js | 39 ++ packages/jest-openapi/__test__/jest.config.js | 3 - .../differentRequestModules.test.js | 378 +++++++++++++++++ .../specsDefineServersDifferently.test.js | 396 ++++++++++++++++-- .../toSatisfyApiSpec/toSatisfyApiSpec.test.js | 22 +- .../toSatisfySchemaInApiSpec.test.js | 2 +- packages/jest-openapi/jest.config.js | 11 + packages/jest-openapi/package.json | 5 +- 17 files changed, 897 insertions(+), 134 deletions(-) delete mode 100644 packages/chai-openapi-response-validator/test/bug-recreation-template.test.js create mode 100644 packages/chai-openapi-response-validator/test/bugRecreationTemplate.test.js create mode 100644 packages/jest-openapi/__test__/bugRecreationTemplate.test.js delete mode 100644 packages/jest-openapi/__test__/jest.config.js create mode 100644 packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/differentRequestModules.test.js create mode 100644 packages/jest-openapi/jest.config.js diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7f411f9..ba308fd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,6 +7,13 @@ assignees: '' --- + + **Which package are you using**? `chai-openapi-response-validator` / `jest-openapi` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12bca33..59792ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,8 @@ A bug is a **recreatable** problem that is caused by the code in the repository. Guidelines for bug reports: -1. **[Check if the issue has already been reported](https://github.com/RuntimeTools/OpenAPIValidators/issues)** -2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. -3. **Write a test recreating the bug** — [use this template to get started quickly](https://github.com/RuntimeTools/OpenAPIValidators/blob/master/packages/chai-openapi-response-validator/test/bug-recreation-template.test.js). +1. **Check if the [issue has already been reported](https://github.com/RuntimeTools/OpenAPIValidators/issues)** +2. **Recreate the bug** — clone `master` and write a quick test. You can use our bug recreation template for [`chai-openapi-response-validator`](https://github.com/RuntimeTools/OpenAPIValidators/blob/master/packages/chai-openapi-response-validator/test/bugRecreationTemplate.test.js) or [`jest-openapi`](https://github.com/RuntimeTools/OpenAPIValidators/blob/master/packages/jest-openapi/__test__/bugRecreationTemplate.test.js). ### Feature Requests diff --git a/commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate/openapi.yml b/commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate/openapi.yml index e5c01c1..a234fc5 100644 --- a/commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate/openapi.yml +++ b/commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate/openapi.yml @@ -1,10 +1,10 @@ openapi: 3.0.0 info: title: Example OpenApi 3 spec - description: Has various paths with responses to use in testing + description: Use to recreate a bug version: 0.1.0 paths: - /test: + /recreate/bug: get: responses: '200': @@ -12,13 +12,7 @@ paths: content: application/json: schema: - allOf: - - type: object - properties: - expectedProperty1: - type: string - - type: object - properties: - expectedProperty2: - type: string - # additionalProperties: false # Uncommenting this line exposes the bug + type: object + properties: + expectedProperty1: + type: string diff --git a/packages/chai-openapi-response-validator/lib/openapi-validator/lib/classes/RequestPromiseResponse.js b/packages/chai-openapi-response-validator/lib/openapi-validator/lib/classes/RequestPromiseResponse.js index fb2943a..2546821 100644 --- a/packages/chai-openapi-response-validator/lib/openapi-validator/lib/classes/RequestPromiseResponse.js +++ b/packages/chai-openapi-response-validator/lib/openapi-validator/lib/classes/RequestPromiseResponse.js @@ -22,7 +22,6 @@ class RequestPromiseResponse extends AbstractResponse { // needs parsing into a JSON object, so just move to the next // block and return the body } - return this.body; } } diff --git a/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/differentRequestModules.test.js b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/differentRequestModules.test.js index 8c3ab50..e0705de 100644 --- a/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/differentRequestModules.test.js +++ b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/differentRequestModules.test.js @@ -128,11 +128,13 @@ describe('Parsing responses from different request modules', function () { }); it('fails when using .not', function () { const assertion = () => expect(res).to.not.satisfyApiSpec; - expect(assertion).to.throw('expected res not to satisfy'); - expect(assertion).to.throw(str({ - body: {}, - text: 'res.body is a string', - })); + expect(assertion).to.throw( + AssertionError, + str({ + body: {}, + text: 'res.body is a string', + }), + ); }); }); @@ -146,11 +148,13 @@ describe('Parsing responses from different request modules', function () { }); it('fails when using .not', function () { const assertion = () => expect(res).to.not.satisfyApiSpec; - expect(assertion).to.throw('expected res not to satisfy'); - expect(assertion).to.throw(str({ - body: {}, - text: '', - })); + expect(assertion).to.throw( + AssertionError, + str({ + body: {}, + text: '', + }), + ); }); }); }); @@ -204,11 +208,13 @@ describe('Parsing responses from different request modules', function () { }); it('fails when using .not', function () { const assertion = () => expect(res).to.not.satisfyApiSpec; - expect(assertion).to.throw('expected res not to satisfy'); - expect(assertion).to.throw(str({ - body: {}, - text: 'res.body is a string', - })); + expect(assertion).to.throw( + AssertionError, + str({ + body: {}, + text: 'res.body is a string', + }), + ); }); }); @@ -241,11 +247,13 @@ describe('Parsing responses from different request modules', function () { }); it('fails when using .not', function () { const assertion = () => expect(res).to.not.satisfyApiSpec; - expect(assertion).to.throw('expected res not to satisfy'); - expect(assertion).to.throw(str({ - body: {}, - text: '', - })); + expect(assertion).to.throw( + AssertionError, + str({ + body: {}, + text: '', + }), + ); }); }); }); @@ -360,6 +368,7 @@ describe('Parsing responses from different request modules', function () { after(function () { app.server.close(); }); + describe('json is set to true, res header is application/json, and res.body is a string', function () { let res; before(async function () { @@ -383,6 +392,7 @@ describe('Parsing responses from different request modules', function () { ); }); }); + describe('res header is application/json, and res.body is a string', function () { let res; before(async function () { diff --git a/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/satisfyApiSpec.test.js b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/satisfyApiSpec.test.js index f6b1e3f..176fa35 100644 --- a/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/satisfyApiSpec.test.js +++ b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/satisfyApiSpec.test.js @@ -42,6 +42,7 @@ openApiSpecs.forEach((spec) => { before(function () { chai.use(chaiResponseValidator(pathToApiSpec)); }); + describe('when \'res\' is not a valid HTTP response object', function () { const res = { status: 204, @@ -187,7 +188,7 @@ openApiSpecs.forEach((spec) => { }); }); - describe('be a object with depth of over 2', function () { + describe('be an object with depth of over 2', function () { const nestedObject = { a: { b: { diff --git a/packages/chai-openapi-response-validator/test/bug-recreation-template.test.js b/packages/chai-openapi-response-validator/test/bug-recreation-template.test.js deleted file mode 100644 index a02b30e..0000000 --- a/packages/chai-openapi-response-validator/test/bug-recreation-template.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/** ***************************************************************************** - * Copyright 2019 IBM Corp. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ****************************************************************************** */ - -const chai = require('chai'); -const path = require('path'); - -const chaiResponseValidator = require('..'); - -const dirContainingApiSpec = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate'); -const { expect } = chai; - -describe('Recreate bug (issue #46)', function () { - before(function () { - const pathToApiSpec = path.join(dirContainingApiSpec, 'openapi.yml'); - chai.use(chaiResponseValidator(pathToApiSpec)); - }); - - const res = { - status: 200, - req: { - method: 'GET', - path: '/test', - }, - body: { - expectedProperty1: 'foo', - expectedProperty2: 'bar', - }, - }; - - it('passes', function () { - expect(res).to.satisfyApiSpec; - }); - - it('fails when using .not', function () { - const assertion = () => expect(res).to.not.satisfyApiSpec; - expect(assertion).to.throw(); - }); -}); diff --git a/packages/chai-openapi-response-validator/test/bugRecreationTemplate.test.js b/packages/chai-openapi-response-validator/test/bugRecreationTemplate.test.js new file mode 100644 index 0000000..eb9b3db --- /dev/null +++ b/packages/chai-openapi-response-validator/test/bugRecreationTemplate.test.js @@ -0,0 +1,34 @@ +const chai = require('chai'); +const path = require('path'); + +const chaiResponseValidator = require('..'); + +const dirContainingApiSpec = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate'); +const { expect } = chai; + +describe('Recreate bug (issue #XX)', function () { + before(function () { + const pathToApiSpec = path.join(dirContainingApiSpec, 'openapi.yml'); + chai.use(chaiResponseValidator(pathToApiSpec)); + }); + + const res = { + status: 200, + req: { + method: 'GET', + path: '/recreate/bug', + }, + body: { + expectedProperty1: 'foo', + }, + }; + + it('passes', function () { + expect(res).to.satisfyApiSpec; + }); + + it('fails when using .not', function () { + const assertion = () => expect(res).to.not.satisfyApiSpec; + expect(assertion).to.throw(); + }); +}); diff --git a/packages/jest-openapi/__test__/.eslintrc.yml b/packages/jest-openapi/__test__/.eslintrc.yml index ea84be4..b954eee 100644 --- a/packages/jest-openapi/__test__/.eslintrc.yml +++ b/packages/jest-openapi/__test__/.eslintrc.yml @@ -7,4 +7,8 @@ env: rules: jest/prefer-expect-assertions: off jest/no-disabled-tests: warn - jest/lowercase-name: off + jest/lowercase-name: + - error + - ignore: + - describe + jest/no-hooks: off diff --git a/packages/jest-openapi/__test__/bugRecreationTemplate.test.js b/packages/jest-openapi/__test__/bugRecreationTemplate.test.js new file mode 100644 index 0000000..01a9d87 --- /dev/null +++ b/packages/jest-openapi/__test__/bugRecreationTemplate.test.js @@ -0,0 +1,39 @@ +const path = require('path'); +const { inspect } = require('util'); + +const jestOpenAPI = require('..'); + +const dirContainingApiSpec = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/bugRecreationTemplate'); + +describe('Recreate bug (issue #XX)', () => { + beforeAll(() => { + const pathToApiSpec = path.join(dirContainingApiSpec, 'openapi.yml'); + jestOpenAPI(pathToApiSpec); + }); + + const res = { + status: 200, + req: { + method: 'GET', + path: '/recreate/bug', + }, + body: { + expectedProperty1: 'foo', + }, + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + inspect({ + body: { + expectedProperty1: 'foo', + }, + }), + ); + }); +}); diff --git a/packages/jest-openapi/__test__/jest.config.js b/packages/jest-openapi/__test__/jest.config.js deleted file mode 100644 index 25c9bac..0000000 --- a/packages/jest-openapi/__test__/jest.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - testEnvironment: 'node', -}; diff --git a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/differentRequestModules.test.js b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/differentRequestModules.test.js new file mode 100644 index 0000000..40bdc78 --- /dev/null +++ b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/differentRequestModules.test.js @@ -0,0 +1,378 @@ +const path = require('path'); +const { inspect } = require('util'); + +const axios = require('axios'); +const supertest = require('supertest'); +const requestPromise = require('request-promise'); + +const jestOpenAPI = require('../../..'); +const app = require('../../../../../commonTestResources/exampleApp'); +const { port } = require('../../../../../commonTestResources/config'); + +const str = (obj) => inspect(obj, { showHidden: false, depth: null }); +const appOrigin = `http://localhost:${port}`; +const pathToApiSpec = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/openapi3.yml'); + +describe('Parsing responses from different request modules', () => { + beforeAll(() => { + jestOpenAPI(pathToApiSpec); + }); + + // These tests cover both supertest and chai-http, because they make requests the same way (using superagent) + describe('supertest', () => { + describe('res header is application/json, and res.body is a string', () => { + let res; + beforeAll(async () => { + res = await supertest(app).get('/test/header/application/json/and/responseBody/string'); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is {}', () => { + let res; + beforeAll(async () => { + res = await supertest(app).get('/test/header/application/json/and/responseBody/emptyObject'); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: {}, + }), + ); + }); + }); + + describe('res header is text/html, res.body is {}, and res.text is a string', () => { + let res; + beforeAll(async () => { + res = await supertest(app).get('/test/header/text/html'); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: {}, + text: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is a null', () => { + let res; + beforeAll(async () => { + res = await supertest(app).get('/test/header/application/json/and/responseBody/nullable'); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: null, + }), + ); + }); + }); + + describe('res has no content-type header, res.body is {}, and res.text is empty string', () => { + let res; + beforeAll(async () => { + res = await supertest(app).get('/test/no/content-type/header/and/no/response/body'); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: {}, + text: '', + }), + ); + }); + }); + }); + + describe('axios', () => { + beforeAll(() => { + app.server = app.listen(port); + }); + afterAll(() => { + app.server.close(); + }); + describe('res header is application/json, and res.body is a string', () => { + let res; + beforeAll(async () => { + res = await axios.get(`${appOrigin}/test/header/application/json/and/responseBody/string`); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is {}', () => { + let res; + beforeAll(async () => { + res = await axios.get(`${appOrigin}/test/header/application/json/and/responseBody/emptyObject`); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: {}, + }), + ); + }); + }); + + describe('res header is text/html, res.body is a string', () => { + let res; + beforeAll(async () => { + res = await axios.get(`${appOrigin}/test/header/text/html`); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is a null', () => { + let res; + beforeAll(async () => { + res = await axios.get(`${appOrigin}/test/header/application/json/and/responseBody/nullable`); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: null, + }), + ); + }); + }); + + describe('res has no content-type header, and res.body is empty string', () => { + let res; + beforeAll(async () => { + res = await axios.get(`${appOrigin}/test/no/content-type/header/and/no/response/body`); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: '', + }), + ); + }); + }); + }); + + describe('request-promise', () => { + beforeAll(() => { + app.server = app.listen(port); + }); + afterAll(() => { + app.server.close(); + }); + + describe('json is set to true, res header is application/json, and res.body is a string', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/application/json/and/responseBody/string`, + resolveWithFullResponse: true, + json: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is a string', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/application/json/and/responseBody/string`, + resolveWithFullResponse: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('json is set to true, res header is application/json, and res.body is {}', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/application/json/and/responseBody/emptyObject`, + resolveWithFullResponse: true, + json: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: {}, + }), + ); + }); + }); + + describe('res header is application/json, and res.body is \'{}\'', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/application/json/and/responseBody/emptyObject`, + resolveWithFullResponse: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: '{}', + }), + ); + }); + }); + + describe('res header is text/html, res.body is a string', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/text/html`, + resolveWithFullResponse: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'res.body is a string', + }), + ); + }); + }); + + describe('res header is application/json, and res.body is a null', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/header/application/json/and/responseBody/nullable`, + resolveWithFullResponse: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: 'null', + }), + ); + }); + }); + + describe('res has no content-type header, and res.body is empty string', () => { + let res; + beforeAll(async () => { + res = await requestPromise({ + method: 'GET', + uri: `${appOrigin}/test/no/content-type/header/and/no/response/body`, + resolveWithFullResponse: true, + }); + }); + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + str({ + body: '', + }), + ); + }); + }); + }); +}); diff --git a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/specsDefineServersDifferently.test.js b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/specsDefineServersDifferently.test.js index 90f08b0..f027832 100644 --- a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/specsDefineServersDifferently.test.js +++ b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/specsDefineServersDifferently.test.js @@ -19,13 +19,56 @@ const green = jestMatcherUtils.EXPECTED_COLOR; const dirContainingApiSpec = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/serversDefinedDifferently'); -describe('using OpenAPI 3 specs that define servers differently', () => { +describe('Using OpenAPI 3 specs that define servers differently', () => { describe('spec has no server property', () => { - beforeAll(() => { // eslint-disable-line jest/no-hooks + beforeAll(() => { const pathToApiSpec = path.join(dirContainingApiSpec, 'noServersProperty.yml'); jestOpenAPI(pathToApiSpec); }); + describe('res.req.path matches the default server (\'/\') and an endpoint path', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(Error, ''); + }); + }); + describe('res.req.path does not match any servers', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: 'nonExistentServer/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `expected ${red('received')} to satisfy a '200' response defined for endpoint 'GET nonExistentServer/test/responseBody/string' in your API spec` + + `\n${red('received')} had request path ${red('nonExistentServer/test/responseBody/string')}, but your API spec has no matching path` + + '\n\nPaths found in API spec:', + // we don't list servers defined in your API spec because there aren't any + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); describe('res.req.path matches the default server (\'/\') but no endpoint paths', () => { const res = { status: 200, @@ -37,13 +80,13 @@ describe('using OpenAPI 3 specs that define servers differently', () => { }; it('fails', () => { - expect.assertions(3); - try { - expect(res).toSatisfyApiSpec(); - } catch (error) { - expect(error.message).toContain('Paths found'); // eslint-disable-line jest/no-try-expect - expect(error.message).not.toContain('Server'); // eslint-disable-line jest/no-try-expect - } + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `expected ${red('received')} to satisfy a '200' response defined for endpoint 'GET /nonExistentEndpointPath' in your API spec` + + `\n${red('received')} had request path ${red('/nonExistentEndpointPath')}, but your API spec has no matching path` + + '\n\nPaths found in API spec:', + // we don't list servers defined in your API spec because there aren't any + ); }); it('passes when using .not', () => { @@ -53,11 +96,54 @@ describe('using OpenAPI 3 specs that define servers differently', () => { }); describe('spec\'s server property is an empty array', () => { - beforeAll(() => { // eslint-disable-line jest/no-hooks + beforeAll(() => { const pathToApiSpec = path.join(dirContainingApiSpec, 'serversIsEmptyArray.yml'); jestOpenAPI(pathToApiSpec); }); + describe('res.req.path matches the default server (\'/\') and an endpoint path', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(Error, ''); + }); + }); + describe('res.req.path does not match any servers', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: 'nonExistentServer/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `expected ${red('received')} to satisfy a '200' response defined for endpoint 'GET nonExistentServer/test/responseBody/string' in your API spec` + + `\n${red('received')} had request path ${red('nonExistentServer/test/responseBody/string')}, but your API spec has no matching path` + + '\n\nPaths found in API spec:', + // we don't list servers defined in your API spec because there aren't any + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); describe('res.req.path matches the default server (\'/\') but no endpoint paths', () => { const res = { status: 200, @@ -69,13 +155,13 @@ describe('using OpenAPI 3 specs that define servers differently', () => { }; it('fails', () => { - expect.assertions(3); - try { - expect(res).toSatisfyApiSpec(); - } catch (error) { - expect(error.message).toContain('Paths found'); // eslint-disable-line jest/no-try-expect - expect(error.message).not.toContain('Server'); // eslint-disable-line jest/no-try-expect - } + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `expected ${red('received')} to satisfy a '200' response defined for endpoint 'GET /nonExistentEndpointPath' in your API spec` + + `\n${red('received')} had request path ${red('/nonExistentEndpointPath')}, but your API spec has no matching path` + + '\n\nPaths found in API spec:', + // we don't list servers defined in your API spec because there aren't any + ); }); it('passes when using .not', () => { @@ -84,10 +170,9 @@ describe('using OpenAPI 3 specs that define servers differently', () => { }); }); - - describe('spec defines servers', () => { - beforeAll(() => { // eslint-disable-line jest/no-hooks - const pathToApiSpec = path.join(dirContainingApiSpec, 'onlyAbsoluteServersWithBasePaths.yml'); + describe('spec defines various (relative and absolute) servers', () => { + beforeAll(() => { + const pathToApiSpec = path.join(dirContainingApiSpec, 'variousServers.yml'); jestOpenAPI(pathToApiSpec); }); @@ -96,21 +181,21 @@ describe('using OpenAPI 3 specs that define servers differently', () => { status: 200, req: { method: 'GET', - path: 'nonExistentServer', + path: 'nonExistentServer/test/responseBody/string', }, body: 'valid body (string)', }; it('fails', () => { const assertion = () => expect(res).toSatisfyApiSpec(); - expect(assertion).toThrow(new Error( + expect(assertion).toThrow( `${expectReceivedToSatisfyApiSpec}` - + `\n\nexpected ${red('received')} to satisfy a '200' response defined for endpoint 'GET nonExistentServer' in your API spec` - + `\n${red('received')} had request path ${red('nonExistentServer')}, but your API spec has no matching path` - + `\n\nPaths found in API spec: ${green('/test/responseBody/string')}` - + '\n\n\'nonExistentServer\' matches no servers' - + '\n\nServers found in API spec: http://api.example.com/basePath1', - )); + + `\n\nexpected ${red('received')} to satisfy a '200' response defined for endpoint 'GET nonExistentServer/test/responseBody/string' in your API spec` + + `\n${red('received')} had request path ${red('nonExistentServer/test/responseBody/string')}, but your API spec has no matching path` + + `\n\nPaths found in API spec: ${green('/test/responseBody/string')}` + + '\n\n\'nonExistentServer/test/responseBody/string\' matches no servers' + + '\n\nServers found in API spec: /relativeServer, /differentRelativeServer', // etc. + ); }); it('passes when using .not', () => { @@ -118,6 +203,130 @@ describe('using OpenAPI 3 specs that define servers differently', () => { }); }); + const tests = { + 'a relative server url': { + serverBasePath: '/relativeServer', + expectedMatchingServers: ['/relativeServer'], + }, + 'a different relative server url': { + serverBasePath: '/differentRelativeServer', + expectedMatchingServers: ['/differentRelativeServer'], + }, + 'multiple server urls': { + serverBasePath: '/relativeServer2', + expectedMatchingServers: ['/relativeServer', '/relativeServer2'], + }, + 'base path of absolute server url with http scheme': { + serverBasePath: '/basePath1', + expectedMatchingServers: ['http://api.example.com/basePath1'], + }, + 'base path of absolute server url with https scheme': { + serverBasePath: '/basePath2', + expectedMatchingServers: ['https://api.example.com/basePath2'], + }, + 'base path of absolute server url with ws scheme': { + serverBasePath: '/basePath3', + expectedMatchingServers: ['ws://api.example.com/basePath3'], + }, + 'base path of absolute server url with wss scheme': { + serverBasePath: '/basePath4', + expectedMatchingServers: ['wss://api.example.com/basePath4'], + }, + 'base path of absolute server url with port': { + serverBasePath: '/basePath5', + expectedMatchingServers: ['http://api.example.com:8443/basePath5'], + }, + 'base path of absolute server url with localhost': { + serverBasePath: '/basePath6', + expectedMatchingServers: ['http://localhost:3025/basePath6'], + }, + 'base path of absolute server url with IPv4 host': { + serverBasePath: '/basePath7', + expectedMatchingServers: ['http://10.0.81.36/basePath7'], + }, + }; + + Object.entries(tests).forEach(([testName, test]) => { + describe(`res.req.path matches ${testName}`, () => { + const { + serverBasePath, + expectedMatchingServers, + } = test; + + describe('res.req.path matches a server and an endpoint path', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: `${serverBasePath}/test/responseBody/string`, + }, + body: 'valid body (string)', + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(Error, ''); + }); + }); + describe('res.req.path matches a server but no endpoint paths', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: `${serverBasePath}/nonExistentEndpointPath`, + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `expected ${red('received')} to satisfy a '200' response defined for endpoint 'GET ${serverBasePath}/nonExistentEndpointPath' in your API spec` + + `\n${red('received')} had request path ${red(`${serverBasePath}/nonExistentEndpointPath`)}, but your API spec has no matching path` + + `\n\nPaths found in API spec: ${green('/test/responseBody/string')}` + + `\n\n'${serverBasePath}/nonExistentEndpointPath' matches servers ${inspect(expectedMatchingServers)} but no combinations` + + '\n\nServers found in API spec: /relativeServer, /differentRelativeServer', // etc. + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); + }); + }); + }); + + describe('spec defines only absolute servers with base paths', () => { + beforeAll(() => { + const pathToApiSpec = path.join(dirContainingApiSpec, 'onlyAbsoluteServersWithBasePaths.yml'); + jestOpenAPI(pathToApiSpec); + }); + + describe('res.req.matches a server base path and an endpoint path', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/basePath1/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(Error, ''); + }); + }); + describe('res.req.path matches a server base path but no endpoint paths', () => { const res = { status: 200, @@ -130,15 +339,126 @@ describe('using OpenAPI 3 specs that define servers differently', () => { it('fails', () => { const assertion = () => expect(res).toSatisfyApiSpec(); - expect(assertion).toThrow(new Error( - `${expectReceivedToSatisfyApiSpec}` - + `\n\nexpected ${red('received')} to satisfy a '200' response defined for endpoint 'GET /basePath1/nonExistentEndpointPath' in your API spec` - + `\n${red('received')} had request path ${red('/basePath1/nonExistentEndpointPath')}, but your API spec has no matching path` - + `\n\nPaths found in API spec: ${green('/test/responseBody/string')}` - + `\n\n'/basePath1/nonExistentEndpointPath' matches servers ${inspect(['http://api.example.com/basePath1'])}` - + ' but no combinations' - + '\n\nServers found in API spec: http://api.example.com/basePath1', - )); + expect(assertion).toThrow( + `'/basePath1/nonExistentEndpointPath' matches servers ${inspect(['http://api.example.com/basePath1'])}` + + ' but no combinations', + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); + + describe('res.req.path does not match any defined server base paths, nor the default base path (\'/\')', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: 'nonExistentServer/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + '\'nonExistentServer/test/responseBody/string\' matches no servers', + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); + + describe('res.req.path does not match any defined server base paths, but does match the default base path (\'/\')', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + '\'/test/responseBody/string\' matches no servers', + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); + }); + + describe('spec defines only absolute servers without base paths', () => { + beforeAll(() => { + const pathToApiSpec = path.join(dirContainingApiSpec, 'noServersWithBasePaths.yml'); + jestOpenAPI(pathToApiSpec); + }); + + describe('res.req.path matches the default server base path (\'/\') and an endpoint path', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(Error, ''); + }); + }); + + describe('res.req.path matches the default server base path (\'/\') but no endpoint paths', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: '/nonExistentEndpointPath', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + `'/nonExistentEndpointPath' matches servers ${inspect(['http://api.example.com'])}` + + ' but no combinations', + ); + }); + + it('passes when using .not', () => { + expect(res).not.toSatisfyApiSpec(); + }); + }); + + describe('res.req.path does not match the default server base path (\'/\') nor any servers', () => { + const res = { + status: 200, + req: { + method: 'GET', + path: 'nonExistentServer/test/responseBody/string', + }, + body: 'valid body (string)', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow( + '\'nonExistentServer/test/responseBody/string\' matches no servers', + ); }); it('passes when using .not', () => { diff --git a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/toSatisfyApiSpec.test.js b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/toSatisfyApiSpec.test.js index 6be0954..c602829 100644 --- a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/toSatisfyApiSpec.test.js +++ b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/toSatisfyApiSpec.test.js @@ -45,9 +45,27 @@ openApiSpecs.forEach((spec) => { const { openApiVersion, pathToApiSpec } = spec; describe(`expect(res).toSatisfyApiSpec() (using an OpenAPI ${openApiVersion} spec)`, () => { - beforeAll(() => { // eslint-disable-line jest/no-hooks + beforeAll(() => { jestOpenAPI(pathToApiSpec); }); + + describe('when \'res\' is not a valid HTTP response object', () => { + const res = { + status: 204, + body: 'should have a \'path\' property', + }; + + it('fails', () => { + const assertion = () => expect(res).toSatisfyApiSpec(); + expect(assertion).toThrow(TypeError); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow(TypeError); + }); + }); + describe('when \'res\' matches a response defined in the API spec', () => { describe('\'res\' satisfies the spec', () => { describe('spec expects res.body to', () => { @@ -176,7 +194,7 @@ openApiSpecs.forEach((spec) => { }); }); - describe('be a object with depth of over 2', () => { + describe('be an object with depth of over 2', () => { const nestedObject = { a: { b: { diff --git a/packages/jest-openapi/__test__/matchers/toSatisfySchemaInApiSpec/toSatisfySchemaInApiSpec.test.js b/packages/jest-openapi/__test__/matchers/toSatisfySchemaInApiSpec/toSatisfySchemaInApiSpec.test.js index 1b67c35..0e707a1 100644 --- a/packages/jest-openapi/__test__/matchers/toSatisfySchemaInApiSpec/toSatisfySchemaInApiSpec.test.js +++ b/packages/jest-openapi/__test__/matchers/toSatisfySchemaInApiSpec/toSatisfySchemaInApiSpec.test.js @@ -45,7 +45,7 @@ openApiSpecs.forEach((spec) => { const { openApiVersion, pathToApiSpec } = spec; describe(`expect(obj).toSatisfySchemaInApiSpec(schemaName) (using an OpenAPI ${openApiVersion} spec)`, () => { - beforeAll(() => { // eslint-disable-line jest/no-hooks + beforeAll(() => { jestOpenAPI(pathToApiSpec); }); diff --git a/packages/jest-openapi/jest.config.js b/packages/jest-openapi/jest.config.js new file mode 100644 index 0000000..7fd7550 --- /dev/null +++ b/packages/jest-openapi/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + testEnvironment: 'node', + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: 100, + }, + }, +}; diff --git a/packages/jest-openapi/package.json b/packages/jest-openapi/package.json index 8c9c289..68cfdeb 100644 --- a/packages/jest-openapi/package.json +++ b/packages/jest-openapi/package.json @@ -42,6 +42,7 @@ "@stryker-mutator/core": "^3.1.0", "@stryker-mutator/javascript-mutator": "^3.1.0", "@stryker-mutator/jest-runner": "^3.1.0", + "axios": "^0.19.2", "eslint": "^6.8.0", "eslint-config-airbnb-base": "^14.1.0", "eslint-plugin-import": "^2.17.2", @@ -49,7 +50,9 @@ "express": "^4.17.1", "fs-extra": "^9.0.0", "jest": "^25.4.0", - "rimraf": "^3.0.2" + "request-promise": "^4.2.5", + "rimraf": "^3.0.2", + "supertest": "^4.0.2" }, "dependencies": { "compress-tag": "^2.0.0",