more fixes and validating with search-backend

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2024-07-07 16:37:12 -04:00
parent 1e952426c1
commit f63ad78082
7 changed files with 72 additions and 17 deletions
@@ -16,6 +16,8 @@
import * as mockttp from 'mockttp';
import { OpenApiProxyValidator } from '../schema/validation';
import getPort from 'get-port';
import { Server } from 'http';
export class Proxy {
server: mockttp.Mockttp;
@@ -25,6 +27,8 @@ export class Proxy {
mockttp.CompletedResponse
>();
validator: OpenApiProxyValidator;
public forwardTo: { port: number } = { port: 0 };
express: { server: Server | undefined } = { server: undefined };
constructor() {
this.server = mockttp.getLocal();
this.validator = new OpenApiProxyValidator();
@@ -32,9 +36,10 @@ export class Proxy {
async setup() {
await this.server.start();
this.forwardTo.port = await getPort();
this.server
.forAnyRequest()
.thenForwardTo(`http://localhost:${process.env.PORT}`);
.thenForwardTo(`http://localhost:${this.forwardTo.port}`);
await this.server.on('request', request => {
this.#openRequests[request.id] = request;
});
@@ -53,10 +58,9 @@ export class Proxy {
});
}
async initialize() {
await this.validator.initialize(
`http://localhost:${process.env.PORT}/openapi.json`,
);
async initialize(url: string, server: Server) {
await this.validator.initialize(`${url}/openapi.json`);
this.express.server = server;
}
stop() {
@@ -64,6 +68,9 @@ export class Proxy {
throw new Error('There are still open requests');
}
this.server.stop();
// If this isn't expressly closed, it will cause a jest memory leak warning.
this.express.server?.close();
}
get url() {
@@ -19,7 +19,12 @@ import { RequestBodyParser } from './request-body-validation';
import Ajv from 'ajv';
import { Operation, RequestParser } from './types';
import _ from 'lodash';
import { OperationObject, RequestBodyObject } from 'openapi3-ts';
import {
ContentObject,
MediaTypeObject,
OperationObject,
RequestBodyObject,
} from 'openapi3-ts';
import { JsonObject } from '@backstage/types';
const ajv = new Ajv();
@@ -79,4 +84,25 @@ describe('request body', () => {
`"["POST /api/search"] No request body found for /api/search"`,
);
});
it('should throw error if request body is not application/json', async () => {
const request = toRequest({}, { 'content-type': 'text/plain' });
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["POST /api/search"] Content type is not application/json"`,
);
});
it('should NOT throw error if request body is not just application/json', async () => {
(schema.requestBody.content as ContentObject)[
'application/json; charset=utf-8'
] = schema.requestBody.content['application/json'] as MediaTypeObject;
delete (schema.requestBody.content as ContentObject)['application/json'];
parser = new RequestBodyParser(operation, {
ajv,
});
const request = toRequest({});
expect(await parser.parse(request)).toEqual({});
});
});
@@ -77,13 +77,17 @@ export class RequestBodyParser
'No content found in request body',
);
}
if (!requestBody!.content['application/json']) {
const contentTypes = requestBody!.content;
const jsonContentType = Object.keys(contentTypes).find(contentType =>
contentType.split(';').includes('application/json'),
);
if (!jsonContentType) {
throw new OperationError(
this.operation,
'No application/json content type found in request body',
);
}
const schema = requestBody!.content['application/json'].schema;
const schema = requestBody!.content[jsonContentType].schema;
if (!schema) {
throw new OperationError(
this.operation,
@@ -111,7 +115,7 @@ export class RequestBodyParser
const contentType =
request.headers.get('content-type') || 'application/json';
if (contentType !== 'application/json') {
if (!contentType.split(';').includes('application/json')) {
throw new OperationError(
this.operation,
'Content type is not application/json',
@@ -61,15 +61,20 @@ export class ResponseBodyParser
this.ajv = options.ajv;
const responseSchemas = operation.schema.responses;
for (const [statusCode, schema] of Object.entries(responseSchemas)) {
if (!schema.content) {
const contentTypes = schema.content;
if (!contentTypes) {
// Skip responses without content, eg 204 No Content.
continue;
} else if (!schema.content['application/json']) {
}
const jsonContentType = Object.keys(contentTypes).find(contentType =>
contentType.split(';').includes('application/json'),
);
if (!jsonContentType) {
throw new OperationError(
this.operation,
`No application/json content type found in response for status code ${statusCode}`,
);
} else if ('$ref' in schema.content['application/json'].schema) {
} else if ('$ref' in contentTypes[jsonContentType].schema) {
throw new OperationError(
this.operation,
'Reference objects are not supported',
@@ -97,21 +102,25 @@ export class ResponseBodyParser
);
}
if (!responseSchema?.content && body?.length) {
const contentTypes = responseSchema.content;
if (!contentTypes && body?.length) {
throw new OperationResponseError(
this.operation,
response,
'Received a body but no schema was found',
);
}
if (!responseSchema?.content!['application/json']) {
const jsonContentType = Object.keys(contentTypes ?? {}).find(contentType =>
contentType.split(';').includes('application/json'),
);
if (!jsonContentType) {
throw new OperationResponseError(
this.operation,
response,
'No application/json content type found in response',
);
}
const schema = responseSchema.content!['application/json'].schema;
const schema = responseSchema.content![jsonContentType].schema;
// This is a bit of type laziness. Ideally, this would be a type-narrowing function, but I wasn't able to get the types to work.
if (!schema) {
throw new OperationError(this.operation, 'No schema found in response');
@@ -28,8 +28,8 @@ afterAll(() => {
});
export async function wrapServer(app: Express): Promise<Server> {
const server = app.listen(+process.env.PORT!);
await proxy.initialize();
const server = app.listen(proxy.forwardTo.port);
await proxy.initialize(`http://localhost:${proxy.forwardTo.port}`, server);
return { ...server, address: () => new URL(proxy.url) } as any;
}