diff --git a/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi2.yml b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi2.yml new file mode 100644 index 00000000..479f9a35 --- /dev/null +++ b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi2.yml @@ -0,0 +1,20 @@ +swagger: '2.0' +info: + title: Test OpenApi 2 spec + description: Test that our plugins prefer to match responses to least-templated paths over most-templated paths + version: 0.1.0 +paths: + ? '/preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/{templatedPath2}' + : get: + responses: + 200: + description: Response body should be a number + schema: + type: number + ? '/preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath' + : get: + responses: + 200: + description: Response body should be a string + schema: + type: string diff --git a/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi3.yml b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi3.yml new file mode 100644 index 00000000..1e8fb6e4 --- /dev/null +++ b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathAfterMostTemplatedPath/openapi3.yml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: Test OpenApi 3 spec + description: Test that our plugins prefer to match responses to least-templated paths over most-templated paths + version: 0.1.0 +paths: + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/{templatedPath2} + : get: + responses: + 200: + description: Response body should be a number + content: + application/json: + schema: + type: number + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath + : get: + responses: + 200: + description: Response body should be a string + content: + application/json: + schema: + type: string diff --git a/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi2.yml b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi2.yml new file mode 100644 index 00000000..e159173a --- /dev/null +++ b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi2.yml @@ -0,0 +1,20 @@ +swagger: '2.0' +info: + title: Test OpenApi 2 spec + description: Test that our plugins prefer to match responses to least-templated paths over most-templated paths + version: 0.1.0 +paths: + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath + : get: + responses: + 200: + description: Response body should be a string + schema: + type: string + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/{templatedPath2} + : get: + responses: + 200: + description: Response body should be a number + schema: + type: number diff --git a/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi3.yml b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi3.yml new file mode 100644 index 00000000..b9157e58 --- /dev/null +++ b/commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath/leastTemplatedPathBeforeMostTemplatedPath/openapi3.yml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: Test OpenApi 3 spec + description: Test that our plugins prefer to match responses to least-templated paths over most-templated paths + version: 0.1.0 +paths: + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath + : get: + responses: + 200: + description: Response body should be a string + content: + application/json: + schema: + type: string + ? /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/{templatedPath2} + : get: + responses: + 200: + description: Response body should be a number + content: + application/json: + schema: + type: number diff --git a/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts new file mode 100644 index 00000000..3374744e --- /dev/null +++ b/packages/chai-openapi-response-validator/test/assertions/satisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts @@ -0,0 +1,67 @@ +import chai from 'chai'; +import path from 'path'; + +import chaiResponseValidator from '../../..'; + +const openApiSpecsDir = path.resolve( + '../../commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath', +); +const { expect } = chai; + +describe('expect(res).to.satisfyApiSpec (using an OpenAPI spec with similar least-templated and most-templated OpenAPI paths)', () => { + [2, 3].forEach((openApiVersion) => { + describe(`OpenAPI ${openApiVersion}`, () => { + const openApiSpecs = [ + { + isLeastTemplatedPathFirst: true, + pathToApiSpec: path.join( + openApiSpecsDir, + 'leastTemplatedPathBeforeMostTemplatedPath', + `openapi${openApiVersion}.yml`, + ), + }, + { + isLeastTemplatedPathFirst: false, + pathToApiSpec: path.join( + openApiSpecsDir, + 'leastTemplatedPathAfterMostTemplatedPath', + `openapi${openApiVersion}.yml`, + ), + }, + ]; + + openApiSpecs.forEach((spec) => { + const { pathToApiSpec, isLeastTemplatedPathFirst } = spec; + + describe(`res.req.path matches a least-templated OpenAPI path ${ + isLeastTemplatedPathFirst ? 'before' : 'after' + } a templated OpenAPI path`, () => { + const res = { + status: 200, + req: { + method: 'GET', + path: + '/preferLeastTemplatedPathOverMostTemplatedPath/templatedPath/nonTemplatedPath', + }, + body: 'valid body (string)', + }; + + before(() => { + chai.use(chaiResponseValidator(pathToApiSpec)); + }); + + it('passes', () => { + expect(res).to.satisfyApiSpec; + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).to.not.satisfyApiSpec; + expect(assertion).to.throw( + "not to satisfy the '200' response defined for endpoint 'GET /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath'", + ); + }); + }); + }); + }); + }); +}); diff --git a/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts new file mode 100644 index 00000000..ccc0bd93 --- /dev/null +++ b/packages/jest-openapi/__test__/matchers/toSatisfyApiSpec/preferLeastTemplatedPathOverMostTemplatedPath.test.ts @@ -0,0 +1,65 @@ +import path from 'path'; + +import jestOpenAPI from '../../..'; + +const openApiSpecsDir = path.resolve( + '../../commonTestResources/exampleOpenApiFiles/valid/preferLeastTemplatedPathOverMostTemplatedPath', +); + +describe('expect(res).toSatisfyApiSpec() (using an OpenAPI spec with similar templated and non-templated OpenAPI paths)', () => { + [2, 3].forEach((openApiVersion) => { + describe(`OpenAPI ${openApiVersion}`, () => { + const openApiSpecs = [ + { + isLeastTemplatedPathFirst: true, + pathToApiSpec: path.join( + openApiSpecsDir, + 'leastTemplatedPathBeforeMostTemplatedPath', + `openapi${openApiVersion}.yml`, + ), + }, + { + isLeastTemplatedPathFirst: false, + pathToApiSpec: path.join( + openApiSpecsDir, + 'leastTemplatedPathAfterMostTemplatedPath', + `openapi${openApiVersion}.yml`, + ), + }, + ]; + + openApiSpecs.forEach((spec) => { + const { pathToApiSpec, isLeastTemplatedPathFirst } = spec; + + describe(`res.req.path matches a non-templated OpenAPI path ${ + isLeastTemplatedPathFirst ? 'before' : 'after' + } a templated OpenAPI path`, () => { + const res = { + status: 200, + req: { + method: 'GET', + path: + '/preferLeastTemplatedPathOverMostTemplatedPath/templatedPath/nonTemplatedPath', + }, + body: 'valid body (string)', + }; + + beforeAll(() => { + jestOpenAPI(pathToApiSpec); + }); + + it('passes', () => { + expect(res).toSatisfyApiSpec(); + }); + + it('fails when using .not', () => { + const assertion = () => expect(res).not.toSatisfyApiSpec(); + expect(assertion).toThrow( + "not to satisfy the '200' response defined for endpoint 'GET /preferLeastTemplatedPathOverMostTemplatedPath/{templatedPath}/nonTemplatedPath'", + ); + }); + }); + }); + }); + }); +}); diff --git a/packages/openapi-validator/lib/utils/common.utils.ts b/packages/openapi-validator/lib/utils/common.utils.ts index d789c7ec..2cc03ed7 100644 --- a/packages/openapi-validator/lib/utils/common.utils.ts +++ b/packages/openapi-validator/lib/utils/common.utils.ts @@ -21,20 +21,29 @@ const doesOpenApiPathMatchPathname = (openApiPath, pathname) => { return doesColonPathMatchPathname(pathInColonForm, pathname); }; +const countPathParams = (openApiPath) => { + return (openApiPath.match(/\{/g) || []).length; +}; + export const findOpenApiPathMatchingPossiblePathnames = ( possiblePathnames, OAPaths, ) => { let openApiPath; + let nbPathParams = -1; // eslint-disable-next-line no-restricted-syntax for (const pathname of possiblePathnames) { // eslint-disable-next-line no-restricted-syntax for (const OAPath of OAPaths) { + const count = countPathParams(OAPath); if (OAPath === pathname) { return OAPath; } if (doesOpenApiPathMatchPathname(OAPath, pathname)) { - openApiPath = OAPath; + if (nbPathParams == -1 || count < nbPathParams) { + nbPathParams = count; + openApiPath = OAPath; + } } } }