Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(57): support multiple routes of same resource #107

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
swagger: '2.0'
info:
title: Test OpenApi 2 spec
description: Test that our plugins prefer to match responses to non-templated paths over templated paths
version: 0.1.0
paths:
/test/preferNonTemplatedPathOverTemplatedPath/{templatedPath}:
get:
responses:
200:
description: Response body should be a number
schema:
type: number
/test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath:
get:
responses:
200:
description: Response body should be a string
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.0
info:
title: Test OpenApi 3 spec
description: Test that our plugins prefer to match responses to non-templated paths over templated paths
version: 0.1.0
paths:
/test/preferNonTemplatedPathOverTemplatedPath/{templatedPath}:
get:
responses:
200:
description: Response body should be a number
content:
application/json:
schema:
type: number
/test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath:
get:
responses:
200:
description: Response body should be a string
content:
application/json:
schema:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
swagger: '2.0'
info:
title: Test OpenApi 2 spec
description: Test that our plugins prefer to match responses to non-templated paths over templated paths
version: 0.1.0
paths:
/test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath:
get:
responses:
200:
description: Response body should be a string
schema:
type: string
/test/preferNonTemplatedPathOverTemplatedPath/{templatedPath}:
get:
responses:
200:
description: Response body should be a number
schema:
type: number
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.0
info:
title: Test OpenApi 3 spec
description: Test that our plugins prefer to match responses to non-templated paths over templated paths
version: 0.1.0
paths:
/test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath:
get:
responses:
200:
description: Response body should be a string
content:
application/json:
schema:
type: string
/test/preferNonTemplatedPathOverTemplatedPath/{templatedPath}:
get:
responses:
200:
description: Response body should be a number
content:
application/json:
schema:
type: number
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ function getExpectedResToSatisfyApiSpecMsg(actualResponse, openApiSpec, validati
+ `\nres had request path '${requestPath}', but your API spec has no matching path`
+ `\n\nPaths found in API spec: ${openApiSpec.paths().join(', ')}`;
if (openApiSpec.didUserDefineServers) {
const matchingServers = openApiSpec.getMatchingServerUrls(requestPath);
msg += (matchingServers.length)
? `\n\n'${requestPath}' matches servers ${stringify(matchingServers)} but no <server/endpointPath> combinations`
: `\n\n'${requestPath}' matches no servers`;
msg += (validationError.code === 'SERVER_NOT_FOUND')
? `\n\n'${requestPath}' matches no servers`
: `\n\n'${requestPath}' matches servers ${stringify(openApiSpec.getMatchingServerUrls(requestPath))} but no <server/endpointPath> combinations`;
msg += `\n\nServers found in API spec: ${openApiSpec.getServerUrls().join(', ')}`;
}
return msg;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ const utils = require('../utils');
const AbstractOpenApiSpec = require('./AbstractOpenApiSpec');
const ValidationError = require('./errors/ValidationError');

const doesOpenApiPathMatchPathname = (openApiPath, pathname) => {
const pathInColonForm = utils.convertOpenApiPathToColonForm(openApiPath);
return utils.doesColonPathMatchPathname(pathInColonForm, pathname);
};

class OpenApi2Spec extends AbstractOpenApiSpec {
findOpenApiPathMatchingPathname(pathname) {
const openApiPath = this.paths().find((OAPath) => doesOpenApiPathMatchPathname(OAPath, pathname));
const openApiPath = utils.findOpenApiPathMatchingPossiblePathnames([pathname], this.paths());
if (!openApiPath) {
throw new ValidationError('PATH_NOT_FOUND');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,10 @@ const serversPropertyNotProvidedOrIsEmptyArray = (spec) => (

const extractBasePath = (inputUrl) => url.parse(inputUrl).path;

const removeBasePathFromPathname = (basePath, pathname) => ((basePath === '/')
const getPathnameWithoutBasePath = (basePath, pathname) => ((basePath === '/')
? pathname
: pathname.replace(basePath, ''));

const doesOpenApiPathMatchPathname = (openApiPath, pathname, matchingServerBasePaths) => {
const pathInColonForm = utils.convertOpenApiPathToColonForm(openApiPath);
const openApiPathMatchesPathname = matchingServerBasePaths.some((basePath) => {
const pathnameWithoutServerBasePath = removeBasePathFromPathname(basePath, pathname);
return utils.doesColonPathMatchPathname(pathInColonForm, pathnameWithoutServerBasePath);
});
return openApiPathMatchesPathname;
};

class OpenApi3Spec extends AbstractOpenApiSpec {
constructor(spec) {
super(spec);
Expand Down Expand Up @@ -74,7 +65,10 @@ class OpenApi3Spec extends AbstractOpenApiSpec {
if (!matchingServerBasePaths.length) {
throw new ValidationError('SERVER_NOT_FOUND');
}
const openApiPath = this.paths().find((OAPath) => doesOpenApiPathMatchPathname(OAPath, pathname, matchingServerBasePaths));
const possiblePathnames = matchingServerBasePaths.map(
(basePath) => getPathnameWithoutBasePath(basePath, pathname),
);
const openApiPath = utils.findOpenApiPathMatchingPossiblePathnames(possiblePathnames, this.paths());
if (!openApiPath) {
throw new ValidationError('PATH_NOT_FOUND');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
const util = require('util');
const { inspect } = require('util');
const { Path: PathParser } = require('path-parser');
const url = require('url');

const isEmptyObj = (obj) => !!obj
&& Object.entries(obj).length === 0
&& obj.constructor === Object;

const stringify = (obj) => util.inspect(
obj,
{ showHidden: false, depth: null },
);
const stringify = (obj) => inspect(obj, { depth: null });

const extractPathname = (actualRequest) => {
const { pathname } = url.parse(actualRequest.path); // excludes the query (because: path = pathname + query)
return pathname;
};

const convertOpenApiPathToColonForm = (openApiPath) => openApiPath
.replace(/{/g, ':')
Expand All @@ -21,15 +23,29 @@ const doesColonPathMatchPathname = (pathInColonForm, pathname) => {
return Boolean(pathParamsInPathname);
};

const extractPathname = (actualRequest) => {
const { pathname } = url.parse(actualRequest.path); // excludes the query (because: path = pathname + query)
return pathname;
const doesOpenApiPathMatchPathname = (openApiPath, pathname) => {
const pathInColonForm = convertOpenApiPathToColonForm(openApiPath);
return doesColonPathMatchPathname(pathInColonForm, pathname);
};

const findOpenApiPathMatchingPossiblePathnames = (possiblePathnames, OAPaths) => {
let openApiPath;
for (const pathname of possiblePathnames) { // eslint-disable-line no-restricted-syntax
for (const OAPath of OAPaths) { // eslint-disable-line no-restricted-syntax
if (OAPath === pathname) {
return OAPath;
}
if (doesOpenApiPathMatchPathname(OAPath, pathname)) {
openApiPath = OAPath;
}
}
}
return openApiPath;
};

module.exports = {
isEmptyObj,
stringify,
convertOpenApiPathToColonForm,
doesColonPathMatchPathname,
extractPathname,
findOpenApiPathMatchingPossiblePathnames,
};
4 changes: 2 additions & 2 deletions packages/chai-openapi-response-validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"test:coverage:browse": "npm run test:coverage; open coverage/lcov-report/index.html",
"test:mutation": "stryker run",
"posttest:mutation": "rimraf commonTestResources",
"test:precommit": "npm run lint && npm run test:coverage && npm run test:mutation",
"test:ci": "npm run test:precommit",
"test:precommit": "npm run lint && npm run test:coverage",
"test:ci": "npm run test:precommit && npm run test:mutation",
"lint": "eslint {lib,test}/**/*.js",
"lint:fix": "npm run lint -- --fix"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const chai = require('chai');
const path = require('path');

const chaiResponseValidator = require('../../..');

const openApiSpecsDir = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/preferNonTemplatedPathOverTemplatedPath');
const { expect } = chai;

describe('expect(res).to.satisfyApiSpec (using an OpenAPI spec with similar templated and non-templated OpenAPI paths)', function () {
[
2,
3,
].forEach((openApiVersion) => {
describe(`OpenAPI ${openApiVersion}`, function () {
const openApiSpecs = [
{
isNonTemplatedPathFirst: true,
pathToApiSpec: path.join(openApiSpecsDir, 'nonTemplatedPathBeforeTemplatedPath', `openapi${openApiVersion}.yml`),
},
{
isNonTemplatedPathFirst: false,
pathToApiSpec: path.join(openApiSpecsDir, 'nonTemplatedPathAfterTemplatedPath', `openapi${openApiVersion}.yml`),
},
];

openApiSpecs.forEach((spec) => {
const {
pathToApiSpec,
isNonTemplatedPathFirst,
} = spec;

describe(`res.req.path matches a non-templated OpenAPI path ${isNonTemplatedPathFirst ? 'before' : 'after'} a templated OpenAPI path`, function () {
const res = {
status: 200,
req: {
method: 'GET',
path: '/test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath',
},
body: 'valid body (string)',
};

before(function () {
chai.use(chaiResponseValidator(pathToApiSpec));
});

it('passes', function () {
expect(res).to.satisfyApiSpec;
});

it('fails when using .not', function () {
const assertion = () => expect(res).to.not.satisfyApiSpec;
expect(assertion).to.throw('not to satisfy the \'200\' response defined for endpoint \'GET /test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath\'');
});
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const path = require('path');

const jestOpenAPI = require('../../..');

const openApiSpecsDir = path.resolve('../../commonTestResources/exampleOpenApiFiles/valid/preferNonTemplatedPathOverTemplatedPath');

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 = [
{
isNonTemplatedPathFirst: true,
pathToApiSpec: path.join(openApiSpecsDir, 'nonTemplatedPathBeforeTemplatedPath', `openapi${openApiVersion}.yml`),
},
{
isNonTemplatedPathFirst: false,
pathToApiSpec: path.join(openApiSpecsDir, 'nonTemplatedPathAfterTemplatedPath', `openapi${openApiVersion}.yml`),
},
];

openApiSpecs.forEach((spec) => {
const {
pathToApiSpec,
isNonTemplatedPathFirst,
} = spec;

describe(`res.req.path matches a non-templated OpenAPI path ${isNonTemplatedPathFirst ? 'before' : 'after'} a templated OpenAPI path`, () => {
const res = {
status: 200,
req: {
method: 'GET',
path: '/test/preferNonTemplatedPathOverTemplatedPath/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 /test/preferNonTemplatedPathOverTemplatedPath/nonTemplatedPath\'');
});
});
});
});
});
});
7 changes: 3 additions & 4 deletions packages/jest-openapi/src/matchers/toSatisfyApiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@ function getExpectReceivedToSatisfyApiSpecMsg(actualResponse, openApiSpec, valid
+ `\n${RECEIVED_COLOR('received')} had request path ${RECEIVED_COLOR(requestPath)}, but your API spec has no matching path`
+ `\n\nPaths found in API spec: ${EXPECTED_COLOR(openApiSpec.paths().join(', '))}`;
if (openApiSpec.didUserDefineServers) {
const matchingServers = openApiSpec.getMatchingServerUrls(requestPath);
msg += (matchingServers.length)
? `\n\n'${requestPath}' matches servers ${stringify(matchingServers)} but no <server/endpointPath> combinations`
: `\n\n'${requestPath}' matches no servers`;
msg += (validationError.code === 'SERVER_NOT_FOUND')
? `\n\n'${requestPath}' matches no servers`
: `\n\n'${requestPath}' matches servers ${stringify(openApiSpec.getMatchingServerUrls(requestPath))} but no <server/endpointPath> combinations`;
msg += `\n\nServers found in API spec: ${openApiSpec.getServerUrls().join(', ')}`;
}
return msg;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ const utils = require('../utils');
const AbstractOpenApiSpec = require('./AbstractOpenApiSpec');
const ValidationError = require('./errors/ValidationError');

const doesOpenApiPathMatchPathname = (openApiPath, pathname) => {
const pathInColonForm = utils.convertOpenApiPathToColonForm(openApiPath);
return utils.doesColonPathMatchPathname(pathInColonForm, pathname);
};

class OpenApi2Spec extends AbstractOpenApiSpec {
findOpenApiPathMatchingPathname(pathname) {
const openApiPath = this.paths().find((OAPath) => doesOpenApiPathMatchPathname(OAPath, pathname));
const openApiPath = utils.findOpenApiPathMatchingPossiblePathnames([pathname], this.paths());
if (!openApiPath) {
throw new ValidationError('PATH_NOT_FOUND');
}
Expand Down
Loading