add tests for path calculations
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -522,4 +522,48 @@ describe('path parameters', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('path parsing', () => {
|
||||
it('should parse a path with a single parameters', async () => {
|
||||
const parsedPath = PathParameterParser.parsePath({
|
||||
operation,
|
||||
schema: '/api/item/{id}',
|
||||
path: '/api/item/test123',
|
||||
});
|
||||
expect(parsedPath).toEqual({ id: 'test123' });
|
||||
});
|
||||
it('should parse a path with multiple parameters', async () => {
|
||||
const parsedPath = PathParameterParser.parsePath({
|
||||
operation,
|
||||
schema: '/api/item/{id}/{name}',
|
||||
path: '/api/item/42/test',
|
||||
});
|
||||
// the string is expected here, but will be optimistically parsed as a number where it makes sense.
|
||||
expect(parsedPath).toEqual({ id: '42', name: 'test' });
|
||||
});
|
||||
|
||||
it('should throw an error if the path does not have enough parts', async () => {
|
||||
expect(() =>
|
||||
PathParameterParser.parsePath({
|
||||
operation,
|
||||
schema: '/api/item/{id}',
|
||||
path: '/api/item',
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"["GET /api/item/{id}"] Path parts do not match"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the path has too many parts', async () => {
|
||||
expect(() =>
|
||||
PathParameterParser.parsePath({
|
||||
operation,
|
||||
schema: '/api/item/{id}',
|
||||
path: '/api/item/test/123',
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"["GET /api/item/{id}"] Path parts do not match"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -359,7 +359,8 @@ export class PathParameterParser
|
||||
}
|
||||
async parse(request: Request) {
|
||||
const { pathname } = new URL(request.url);
|
||||
const params = this.parsePath({
|
||||
const params = PathParameterParser.parsePath({
|
||||
operation: this.operation,
|
||||
path: pathname,
|
||||
schema: this.operation.path,
|
||||
});
|
||||
@@ -393,11 +394,19 @@ export class PathParameterParser
|
||||
return pathParameters;
|
||||
}
|
||||
|
||||
parsePath({ schema, path }: { schema: string; path: string }) {
|
||||
static parsePath({
|
||||
operation,
|
||||
schema,
|
||||
path,
|
||||
}: {
|
||||
operation: Operation;
|
||||
schema: string;
|
||||
path: string;
|
||||
}) {
|
||||
const parts = path.split('/');
|
||||
const pathParts = schema.split('/');
|
||||
if (parts.length !== pathParts.length) {
|
||||
throw new OperationError(this.operation, 'Path parts do not match');
|
||||
throw new OperationError(operation, 'Path parts do not match');
|
||||
}
|
||||
const params: Record<string, string> = {};
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OpenApiProxyValidator } from './validation';
|
||||
import { findOperationByRequest, OpenApiProxyValidator } from './validation';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
@@ -22,7 +22,7 @@ import { CompletedBody, CompletedRequest, CompletedResponse } from 'mockttp';
|
||||
import withResponseBody from './__fixtures__/schemas/withJsonResponseBody.json';
|
||||
import withQueryParameter from './__fixtures__/schemas/withQueryParameter.json';
|
||||
import _ from 'lodash';
|
||||
import { ParameterObject } from 'openapi3-ts';
|
||||
import { OpenAPIObject, ParameterObject } from 'openapi3-ts';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
@@ -709,3 +709,115 @@ describe('OpenApiProxyValidator', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOperationByRequest', () => {
|
||||
it('finds an operation by request', () => {
|
||||
const schema = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Test',
|
||||
version: '1.0.0',
|
||||
},
|
||||
paths: {
|
||||
'/api/search': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8080/api/search',
|
||||
} as CompletedRequest;
|
||||
expect(findOperationByRequest(schema as OpenAPIObject, request)).toEqual([
|
||||
'/api/search',
|
||||
schema.paths['/api/search'].get,
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds an operation by request when there are multiple other paths', () => {
|
||||
const schema = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Test',
|
||||
version: '1.0.0',
|
||||
},
|
||||
paths: {
|
||||
'/api/search': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
'/api/catalog/by-ref': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8080/api/search',
|
||||
} as CompletedRequest;
|
||||
expect(findOperationByRequest(schema as OpenAPIObject, request)).toEqual([
|
||||
'/api/search',
|
||||
schema.paths['/api/search'].get,
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds an operation by request when there are path parameters', () => {
|
||||
const schema = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Test',
|
||||
version: '1.0.0',
|
||||
},
|
||||
paths: {
|
||||
'/api/catalog/by-id/{id}': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8080/api/catalog/by-id/123',
|
||||
} as CompletedRequest;
|
||||
expect(findOperationByRequest(schema as OpenAPIObject, request)).toEqual([
|
||||
'/api/catalog/by-id/{id}',
|
||||
schema.paths['/api/catalog/by-id/{id}'].get,
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds an operation by request when there are somewhat overlapping path parameters', () => {
|
||||
const schema = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Test',
|
||||
version: '1.0.0',
|
||||
},
|
||||
paths: {
|
||||
'/api/catalog/by-id/{id}': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
'/api/catalog/by-id': {
|
||||
get: {
|
||||
parameters: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const request = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8080/api/catalog/by-id/123',
|
||||
} as CompletedRequest;
|
||||
expect(findOperationByRequest(schema as OpenAPIObject, request)).toEqual([
|
||||
'/api/catalog/by-id/{id}',
|
||||
schema.paths['/api/catalog/by-id/{id}'].get,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,49 @@ class ResponseBodyValidator implements Validator {
|
||||
}
|
||||
}
|
||||
|
||||
export function findOperationByRequest(
|
||||
openApiSchema: OpenAPIObject,
|
||||
request: CompletedRequest,
|
||||
): [string, OperationObject] | undefined {
|
||||
const { url } = request;
|
||||
const { pathname } = new URL(url);
|
||||
|
||||
const parts = pathname.split('/');
|
||||
for (const [path, schema] of Object.entries(openApiSchema.paths)) {
|
||||
const pathParts = path.split('/');
|
||||
if (parts.length !== pathParts.length) {
|
||||
continue;
|
||||
}
|
||||
let found = true;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (pathParts[i] === parts[i]) {
|
||||
continue;
|
||||
}
|
||||
if (pathParts[i].startsWith('{') && pathParts[i].endsWith('}')) {
|
||||
continue;
|
||||
}
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
continue;
|
||||
}
|
||||
let matchingOperationType: OperationObject | undefined = undefined;
|
||||
for (const [operationType, operation] of Object.entries(schema)) {
|
||||
if (operationType === request.method.toLowerCase()) {
|
||||
matchingOperationType = operation as OperationObject;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchingOperationType) {
|
||||
continue;
|
||||
}
|
||||
return [path, matchingOperationType];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export class OpenApiProxyValidator {
|
||||
schema: OpenAPIObject | undefined;
|
||||
validators: Validator[] | undefined;
|
||||
@@ -74,7 +117,7 @@ export class OpenApiProxyValidator {
|
||||
}
|
||||
|
||||
async validate(request: CompletedRequest, response: CompletedResponse) {
|
||||
const operationPathTuple = this.findOperation(request);
|
||||
const operationPathTuple = findOperationByRequest(this.schema!, request);
|
||||
if (!operationPathTuple) {
|
||||
throw new OperationError(
|
||||
{ path: request.path, method: request.method } as Operation,
|
||||
@@ -95,46 +138,4 @@ export class OpenApiProxyValidator {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private findOperation(
|
||||
request: CompletedRequest,
|
||||
): [string, OperationObject] | undefined {
|
||||
const { url } = request;
|
||||
const { pathname } = new URL(url);
|
||||
|
||||
const parts = pathname.split('/');
|
||||
for (const [path, schema] of Object.entries(this.schema!.paths)) {
|
||||
const pathParts = path.split('/');
|
||||
if (parts.length !== pathParts.length) {
|
||||
continue;
|
||||
}
|
||||
let found = true;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (pathParts[i] === parts[i]) {
|
||||
continue;
|
||||
}
|
||||
if (pathParts[i].startsWith('{') && pathParts[i].endsWith('}')) {
|
||||
continue;
|
||||
}
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
continue;
|
||||
}
|
||||
let matchingOperationType: OperationObject | undefined = undefined;
|
||||
for (const [operationType, operation] of Object.entries(schema)) {
|
||||
if (operationType === request.method.toLowerCase()) {
|
||||
matchingOperationType = operation as OperationObject;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchingOperationType) {
|
||||
continue;
|
||||
}
|
||||
return [path, matchingOperationType];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user