feat(openapi-tooling): custom validation server for test cases
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -33,15 +33,18 @@
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"ajv": "^8.16.0",
|
||||
"express": "^4.17.1",
|
||||
"express-openapi-validator": "^5.0.4",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"json-schema-to-ts": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mockttp": "^3.13.0",
|
||||
"openapi-merge": "^1.3.2",
|
||||
"openapi3-ts": "^3.1.2"
|
||||
},
|
||||
|
||||
@@ -32,4 +32,4 @@ export type {
|
||||
} from './utility';
|
||||
export type { ApiRouter } from './router';
|
||||
export { createValidatedOpenApiRouter, getOpenApiSpecRoute } from './stub';
|
||||
export { wrapInOpenApiTestServer } from './testUtils';
|
||||
export { wrapInOpenApiTestServer, wrapServer } from './testUtils';
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as mockttp from 'mockttp';
|
||||
import { OpenApiProxyValidator } from '../schema/validation';
|
||||
|
||||
export class Proxy {
|
||||
server: mockttp.Mockttp;
|
||||
#openRequests: Record<string, mockttp.CompletedRequest> = {};
|
||||
requestResponsePairs = new Map<
|
||||
mockttp.CompletedRequest,
|
||||
mockttp.CompletedResponse
|
||||
>();
|
||||
validator: OpenApiProxyValidator;
|
||||
constructor() {
|
||||
this.server = mockttp.getLocal();
|
||||
this.validator = new OpenApiProxyValidator();
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.server.start();
|
||||
this.server
|
||||
.forAnyRequest()
|
||||
.thenForwardTo(`http://localhost:${process.env.PORT}`);
|
||||
await this.server.on('request', request => {
|
||||
this.#openRequests[request.id] = request;
|
||||
});
|
||||
await this.server.on('response', response => {
|
||||
const request = this.#openRequests[response.id];
|
||||
if (request) {
|
||||
this.requestResponsePairs.set(request, response);
|
||||
}
|
||||
delete this.#openRequests[response.id];
|
||||
try {
|
||||
this.validator.validate(request, response);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.validator.initialize(
|
||||
`http://localhost:${process.env.PORT}/openapi.json`,
|
||||
);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (Object.keys(this.#openRequests).length > 0) {
|
||||
throw new Error('There are still open requests');
|
||||
}
|
||||
this.server.stop();
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.server.proxyEnv.HTTP_PROXY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import { CompletedRequest, CompletedResponse } from 'mockttp';
|
||||
import {
|
||||
OpenAPIObject,
|
||||
OperationObject,
|
||||
ParameterObject,
|
||||
ResponseObject,
|
||||
SchemaObject,
|
||||
} from 'openapi3-ts';
|
||||
import Ajv from 'ajv';
|
||||
import Parser from '@apidevtools/swagger-parser';
|
||||
|
||||
const ajv = new Ajv({ allErrors: true }); // options can be passed, e.g. {allErrors: true}
|
||||
|
||||
interface RequestResponsePair {
|
||||
request: CompletedRequest;
|
||||
response: CompletedResponse;
|
||||
}
|
||||
|
||||
interface ValidatorParams {
|
||||
pair: RequestResponsePair;
|
||||
operationSchema: OperationObject;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface Validator {
|
||||
validate(pair: ValidatorParams): Promise<void>;
|
||||
}
|
||||
|
||||
class RequestErrorFactory {
|
||||
static createRequestError(request: CompletedRequest, message: string): Error {
|
||||
return new Error(`[${request.url} (${request.method})]: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ParameterValidator implements Validator {
|
||||
schema: OpenAPIObject;
|
||||
cache: Record<string, any> = {};
|
||||
constructor(schema: OpenAPIObject) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
async validate({
|
||||
pair: { request, response },
|
||||
operationSchema,
|
||||
path,
|
||||
}: ValidatorParams) {
|
||||
if (response.statusCode === 400) {
|
||||
// If the response is a 400, then the request is invalid and we shouldn't validate the parameters
|
||||
return;
|
||||
}
|
||||
const parameters = operationSchema.parameters;
|
||||
const queryParameters: Record<string, ParameterObject> = {};
|
||||
const headerParameters: Record<string, ParameterObject> = {};
|
||||
const pathParameters: Record<string, ParameterObject> = {};
|
||||
for (const parameter of parameters || []) {
|
||||
if ('$ref' in parameter) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported',
|
||||
);
|
||||
}
|
||||
if (parameter.in === 'query') {
|
||||
queryParameters[parameter.name] = parameter;
|
||||
}
|
||||
if (parameter.in === 'header') {
|
||||
headerParameters[parameter.name] = parameter;
|
||||
}
|
||||
if (parameter.in === 'path') {
|
||||
pathParameters[parameter.name] = parameter;
|
||||
}
|
||||
}
|
||||
this.validateQueryParameters(queryParameters, request);
|
||||
this.validateHeaderParameters(headerParameters, request);
|
||||
this.validatePathParameters(pathParameters, request, path);
|
||||
}
|
||||
|
||||
validateQueryParameters(
|
||||
queryParameters: Record<string, ParameterObject>,
|
||||
request: CompletedRequest,
|
||||
) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
for (const [name, parameter] of Object.entries(queryParameters)) {
|
||||
if (!parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Schema not found for query parameter',
|
||||
);
|
||||
}
|
||||
if ('$ref' in parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported for parameters',
|
||||
);
|
||||
}
|
||||
let param: any | null = this.#findQueryParameters(
|
||||
request,
|
||||
queryParameters,
|
||||
searchParams,
|
||||
name,
|
||||
);
|
||||
if (parameter.schema.type !== 'array' && Array.isArray(param)) {
|
||||
param = param.length > 0 ? param[0] : undefined;
|
||||
}
|
||||
|
||||
if (!param && parameter.required) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`Required query parameter ${name} not found`,
|
||||
);
|
||||
} else if (!param && !parameter.required) {
|
||||
continue;
|
||||
}
|
||||
if (parameter.schema.type === 'integer') {
|
||||
// Try to parse the integer as AJV won't do it for us.
|
||||
param = parseInt(param, 10);
|
||||
}
|
||||
const validate = ajv.compile(parameter.schema);
|
||||
const valid = validate(param);
|
||||
if (!valid) {
|
||||
console.log(param);
|
||||
console.error(validate.errors);
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Query parameter validation failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#findQueryParameters(
|
||||
request: CompletedRequest,
|
||||
parameters: Record<string, ParameterObject>,
|
||||
searchParams: URLSearchParams,
|
||||
name: string,
|
||||
) {
|
||||
const parameter = parameters[name];
|
||||
const schema = parameter.schema as SchemaObject;
|
||||
if (schema.type === 'array') {
|
||||
if (parameter.style === 'form' || !parameter.style) {
|
||||
if (parameter.explode || typeof parameter.explode === 'undefined') {
|
||||
if (!searchParams.has(name) && searchParams.has(`${name}[0]`)) {
|
||||
const values: string[] = [];
|
||||
let index = 0;
|
||||
while (searchParams.has(`${name}[${index}]`)) {
|
||||
values.push(searchParams.get(`${name}[${index}]`)!);
|
||||
index++;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
return searchParams.getAll(name);
|
||||
}
|
||||
if (!searchParams.has(name) && searchParams.has(`${name}[]`)) {
|
||||
return searchParams.getAll(`${name}[]`);
|
||||
}
|
||||
return searchParams.get(name)?.split(',');
|
||||
} else if (parameter.style === 'spaceDelimited') {
|
||||
return searchParams.get(name)?.split(' ');
|
||||
} else if (parameter.style === 'pipeDelimited') {
|
||||
return searchParams.get(name)?.split('|');
|
||||
}
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Unsupported style for array parameter',
|
||||
);
|
||||
}
|
||||
if (schema.type === 'object') {
|
||||
if (parameter.style === 'form' || !parameter.style) {
|
||||
if (parameter.explode) {
|
||||
const obj: Record<string, string> = {};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
if (this.#matchesOtherQueryParameters(parameters, key)) {
|
||||
continue;
|
||||
}
|
||||
obj[key] = value;
|
||||
}
|
||||
console.log(obj);
|
||||
return obj;
|
||||
}
|
||||
const obj: Record<string, string> = {};
|
||||
const value = searchParams.get(name);
|
||||
if (value) {
|
||||
const parts = value.split(',');
|
||||
if (parts.length % 2 !== 0) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Invalid object parameter',
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < parts.length; i += 2) {
|
||||
obj[parts[i]] = parts[i + 1];
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
} else if (parameter.style === 'deepObject') {
|
||||
const obj: Record<string, any> = {};
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
if (key.startsWith(`${name}[`)) {
|
||||
const parts = key.split('[');
|
||||
let currentLayer = obj;
|
||||
for (let partIndex = 0; partIndex < parts.length - 1; partIndex++) {
|
||||
const part = parts[partIndex];
|
||||
const objKey = part.split(']')[0];
|
||||
if (!currentLayer[objKey]) {
|
||||
currentLayer[objKey] = {};
|
||||
}
|
||||
currentLayer = currentLayer[objKey];
|
||||
}
|
||||
currentLayer[parts[parts.length - 1].split(']')[0]] = value;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Unsupported style for object parameter',
|
||||
);
|
||||
}
|
||||
// For everything else, just return the value.
|
||||
return searchParams.getAll(name);
|
||||
}
|
||||
|
||||
#matchesOtherQueryParameters(
|
||||
parameters: Record<string, ParameterObject>,
|
||||
nameToMatch: string,
|
||||
) {
|
||||
for (const [name] of Object.entries(parameters)) {
|
||||
if (name === nameToMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
validateHeaderParameters(
|
||||
headerParameters: Record<string, ParameterObject>,
|
||||
request: CompletedRequest,
|
||||
) {
|
||||
for (const [name, parameter] of Object.entries(headerParameters)) {
|
||||
if (!request.headers[name]) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`Header parameter ${name} not found`,
|
||||
);
|
||||
}
|
||||
if (!parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Schema not found for path parameter',
|
||||
);
|
||||
}
|
||||
if ('$ref' in parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported for parameters',
|
||||
);
|
||||
}
|
||||
const validate = ajv.compile(parameter.schema);
|
||||
const valid = validate(request.headers[name]);
|
||||
|
||||
if (!valid) {
|
||||
console.log(request.headers[name]);
|
||||
console.error(validate.errors);
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Header parameter validation failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validatePathParameters(
|
||||
pathParameters: Record<string, ParameterObject>,
|
||||
request: CompletedRequest,
|
||||
path: string,
|
||||
) {
|
||||
const { pathname } = new URL(request.url);
|
||||
const params = parsePath({ request, path: pathname, schema: path });
|
||||
for (const [name, parameter] of Object.entries(pathParameters)) {
|
||||
if (!params[name] && parameter.required) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`Path parameter ${name} not found`,
|
||||
);
|
||||
}
|
||||
if (!parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Schema not found for path parameter',
|
||||
);
|
||||
}
|
||||
if ('$ref' in parameter.schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported for parameters',
|
||||
);
|
||||
}
|
||||
|
||||
const validate = ajv.compile(parameter.schema);
|
||||
const valid = validate(params[name]);
|
||||
|
||||
if (!valid) {
|
||||
console.log(params);
|
||||
console.error(validate.errors);
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Path parameter validation failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parsePath({
|
||||
request,
|
||||
schema,
|
||||
path,
|
||||
}: {
|
||||
request: CompletedRequest;
|
||||
schema: string;
|
||||
path: string;
|
||||
}) {
|
||||
const parts = path.split('/');
|
||||
const pathParts = schema.split('/');
|
||||
if (parts.length !== pathParts.length) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Path parts do not match',
|
||||
);
|
||||
}
|
||||
const params: Record<string, string> = {};
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (pathParts[i] === parts[i]) {
|
||||
continue;
|
||||
}
|
||||
if (pathParts[i].startsWith('{') && pathParts[i].endsWith('}')) {
|
||||
params[pathParts[i].slice(1, -1)] = parts[i];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export class RequestBodyValidator implements Validator {
|
||||
schema: OpenAPIObject;
|
||||
constructor(schema: OpenAPIObject) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
async validate({
|
||||
pair: { request, response },
|
||||
operationSchema,
|
||||
}: ValidatorParams) {
|
||||
if (response.statusCode === 400) {
|
||||
// If the response is a 400, then the request is invalid and we shouldn't validate the request body
|
||||
return;
|
||||
}
|
||||
const requestBody = operationSchema.requestBody;
|
||||
const bodyText = await request.body.getText();
|
||||
if (!requestBody && bodyText?.length) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`No request body found for ${request.url}`,
|
||||
);
|
||||
} else if (!requestBody && !bodyText?.length) {
|
||||
// If there is no request body in the schema and no body in the request, then the request is valid
|
||||
return;
|
||||
}
|
||||
if ('$ref' in requestBody!) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported',
|
||||
);
|
||||
}
|
||||
if (!requestBody!.content) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No content found in request body',
|
||||
);
|
||||
}
|
||||
if (!requestBody!.content['application/json']) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No application/json content type found in request body',
|
||||
);
|
||||
}
|
||||
const contentType = request.headers['content-type'];
|
||||
if (!contentType) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Content type not found in request',
|
||||
);
|
||||
}
|
||||
if (contentType !== 'application/json') {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Content type is not application/json',
|
||||
);
|
||||
}
|
||||
const schema = requestBody!.content['application/json'].schema;
|
||||
if (!schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No schema found in request body',
|
||||
);
|
||||
}
|
||||
if ('$ref' in schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported',
|
||||
);
|
||||
}
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
const body = await request.body.getJson();
|
||||
const valid = validate(body);
|
||||
if (!valid) {
|
||||
console.log(body);
|
||||
console.error(validate.errors);
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`Request body validation failed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseBodyValidator implements Validator {
|
||||
schema: OpenAPIObject;
|
||||
constructor(schema: OpenAPIObject) {
|
||||
this.schema = schema;
|
||||
}
|
||||
|
||||
async validate(pair: ValidatorParams) {
|
||||
const {
|
||||
pair: { response, request },
|
||||
operationSchema,
|
||||
} = pair;
|
||||
const responseSchema = this.findResponseSchema(operationSchema, response);
|
||||
if (!responseSchema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`No response schema found for ${response.statusCode}`,
|
||||
);
|
||||
}
|
||||
const body = await response.body.getText();
|
||||
if (!responseSchema.content && body?.length) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No content found in response',
|
||||
);
|
||||
} else if (!responseSchema.content && !body?.length) {
|
||||
// If there is no content in the response schema and no body in the response, then the response is valid
|
||||
return;
|
||||
}
|
||||
if (!responseSchema.content!['application/json']) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No application/json content type found in response',
|
||||
);
|
||||
}
|
||||
const schema = responseSchema.content!['application/json'].schema;
|
||||
if (!schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'No schema found in response',
|
||||
);
|
||||
}
|
||||
if ('$ref' in schema) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Reference objects are not supported',
|
||||
);
|
||||
}
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(await response.body.getJson());
|
||||
if (!valid) {
|
||||
console.log(await response.body.getJson());
|
||||
console.error(validate.errors);
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
'Response body validation failed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private findResponseSchema(
|
||||
operationSchema: OperationObject,
|
||||
response: CompletedResponse,
|
||||
): ResponseObject | undefined {
|
||||
const { statusCode } = response;
|
||||
return operationSchema.responses?.[statusCode];
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenApiProxyValidator {
|
||||
schema: OpenAPIObject | undefined;
|
||||
validators: Validator[] | undefined;
|
||||
|
||||
async initialize(url: string) {
|
||||
this.schema = (await Parser.dereference(url)) as unknown as OpenAPIObject;
|
||||
this.validators = [
|
||||
new ParameterValidator(this.schema),
|
||||
new RequestBodyValidator(this.schema),
|
||||
// new ResponseBodyValidator(this.schema),
|
||||
];
|
||||
}
|
||||
|
||||
validate(request: CompletedRequest, response: CompletedResponse) {
|
||||
const operation = this.findOperation(request);
|
||||
if (!operation) {
|
||||
throw RequestErrorFactory.createRequestError(
|
||||
request,
|
||||
`No operation schema found for ${request.url}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [path, operationSchema] = operation;
|
||||
|
||||
const validators = this.validators!;
|
||||
for (const validator of validators) {
|
||||
validator.validate({
|
||||
pair: { request, response },
|
||||
operationSchema,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,23 @@
|
||||
*/
|
||||
import { Express } from 'express';
|
||||
import { Server } from 'http';
|
||||
import { Proxy } from './proxy/setup';
|
||||
|
||||
const proxy = new Proxy();
|
||||
|
||||
beforeAll(async () => {
|
||||
await proxy.setup();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
proxy.stop();
|
||||
});
|
||||
|
||||
export async function wrapServer(app: Express): Promise<Server> {
|
||||
const server = app.listen(+process.env.PORT!);
|
||||
await proxy.initialize();
|
||||
return { ...server, address: () => new URL(proxy.url) } as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* !!! THIS CURRENTLY ONLY SUPPORTS SUPERTEST !!!
|
||||
|
||||
@@ -38,7 +38,7 @@ import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/a
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
import { z } from 'zod';
|
||||
import { decodeCursor, encodeCursor } from './util';
|
||||
import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
|
||||
import { wrapServer } from '@backstage/backend-openapi-utils';
|
||||
import { Server } from 'http';
|
||||
import { mockCredentials, mockServices } from '@backstage/backend-test-utils';
|
||||
import { LocationAnalyzer } from '@backstage/plugin-catalog-node';
|
||||
@@ -93,7 +93,7 @@ describe('createRouter readonly disabled', () => {
|
||||
locationAnalyzer,
|
||||
permissionsService: permissionsService,
|
||||
});
|
||||
app = wrapInOpenApiTestServer(express().use(router));
|
||||
app = await wrapServer(express().use(router));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -207,7 +207,7 @@ export const spec = {
|
||||
name: 'filters',
|
||||
in: 'query',
|
||||
required: false,
|
||||
style: 'deepObject',
|
||||
style: 'form',
|
||||
explode: true,
|
||||
allowReserved: true,
|
||||
schema: {
|
||||
|
||||
@@ -139,7 +139,7 @@ paths:
|
||||
- name: filters
|
||||
in: query
|
||||
required: false
|
||||
style: deepObject
|
||||
style: form
|
||||
explode: true
|
||||
allowReserved: true
|
||||
schema:
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { createRouter } from './router';
|
||||
import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
|
||||
import { wrapServer } from '@backstage/backend-openapi-utils';
|
||||
import { Server } from 'http';
|
||||
import {
|
||||
mockCredentials,
|
||||
@@ -87,9 +87,7 @@ describe('createRouter', () => {
|
||||
auth: mockServices.auth(),
|
||||
httpAuth: mockServices.httpAuth(),
|
||||
});
|
||||
app = wrapInOpenApiTestServer(
|
||||
express().use(router).use(mockErrorHandler()),
|
||||
);
|
||||
app = await wrapServer(express().use(router).use(mockErrorHandler()));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -3738,16 +3738,19 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/backend-openapi-utils@workspace:packages/backend-openapi-utils"
|
||||
dependencies:
|
||||
"@apidevtools/swagger-parser": ^10.1.0
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@types/express": ^4.17.6
|
||||
"@types/express-serve-static-core": ^4.17.5
|
||||
ajv: ^8.16.0
|
||||
express: ^4.17.1
|
||||
express-openapi-validator: ^5.0.4
|
||||
express-promise-router: ^4.1.0
|
||||
json-schema-to-ts: ^3.0.0
|
||||
lodash: ^4.17.21
|
||||
mockttp: ^3.13.0
|
||||
openapi-merge: ^1.3.2
|
||||
openapi3-ts: ^3.1.2
|
||||
supertest: ^7.0.0
|
||||
@@ -10313,6 +10316,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@httptoolkit/httpolyglot@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "@httptoolkit/httpolyglot@npm:2.2.1"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 5b3882657e37953bd7089d91ac6cd24cec36480deab114e6b69a4b3d9e4ab09db568500e5e96713869fb4a8fe40b5ecc1661cc39ee621ef40ed0e38b55e0257e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@httptoolkit/subscriptions-transport-ws@npm:^0.11.2":
|
||||
version: 0.11.2
|
||||
resolution: "@httptoolkit/subscriptions-transport-ws@npm:0.11.2"
|
||||
@@ -20715,7 +20727,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.17.1, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0":
|
||||
"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.16.0, ajv@npm:^8.17.1, ajv@npm:^8.6.0, ajv@npm:^8.6.3, ajv@npm:^8.9.0":
|
||||
version: 8.17.1
|
||||
resolution: "ajv@npm:8.17.1"
|
||||
dependencies:
|
||||
@@ -21339,6 +21351,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-mutex@npm:^0.5.0":
|
||||
version: 0.5.0
|
||||
resolution: "async-mutex@npm:0.5.0"
|
||||
dependencies:
|
||||
tslib: ^2.4.0
|
||||
checksum: be1587f4875f3bb15e34e9fcce82eac2966daef4432c8d0046e61947fb9a1b95405284601bc7ce4869319249bc07c75100880191db6af11d1498931ac2a2f9ea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-retry@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "async-retry@npm:1.3.3"
|
||||
@@ -22111,6 +22132,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brotli-wasm@npm:^3.0.0":
|
||||
version: 3.0.1
|
||||
resolution: "brotli-wasm@npm:3.0.1"
|
||||
checksum: 48191b27265de8ffc59c940f9efef3a931448b6a15c26a4e360192fc3f0968e073c11fe0926510d019c305cc1d9c6d65df4d3e5752648a91cb0bbcccff7a8460
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"browser-headers@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "browser-headers@npm:0.4.1"
|
||||
@@ -24995,6 +25023,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"destroyable-server@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "destroyable-server@npm:1.0.2"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 81fd70b9132d43c3633a7a819adfe1fc68b52a55154ff8a36f42f4655e7b71b8468559888caadfd324c1aa824f0d236796a8f356e8a00e7438649e647ea654b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-indent@npm:^6.0.0":
|
||||
version: 6.1.0
|
||||
resolution: "detect-indent@npm:6.1.0"
|
||||
@@ -29301,6 +29338,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-encoding@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "http-encoding@npm:2.0.1"
|
||||
dependencies:
|
||||
brotli-wasm: ^3.0.0
|
||||
pify: ^5.0.0
|
||||
zstd-codec: ^0.1.5
|
||||
checksum: c34a1cd81ad1c08e6c6aba5aef3f4d4bc4a6c84f8b3511776eb62006beeee48a104ce1630e3c8497f66d5c0913195dea596e776336dd5a598bd7fe06d27e1395
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-errors@npm:2.0.0, http-errors@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "http-errors@npm:2.0.0"
|
||||
@@ -29459,6 +29507,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http2-wrapper@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "http2-wrapper@npm:2.2.1"
|
||||
dependencies:
|
||||
quick-lru: ^5.1.1
|
||||
resolve-alpn: ^1.2.0
|
||||
checksum: e95e55e22c6fd61182ce81fecb9b7da3af680d479febe8ad870d05f7ebbc9f076e455193766f4e7934e50913bf1d8da3ba121fb5cd2928892390b58cf9d5c509
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-browserify@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "https-browserify@npm:1.0.0"
|
||||
@@ -34535,6 +34593,58 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mockttp@npm:^3.13.0":
|
||||
version: 3.15.2
|
||||
resolution: "mockttp@npm:3.15.2"
|
||||
dependencies:
|
||||
"@graphql-tools/schema": ^8.5.0
|
||||
"@graphql-tools/utils": ^8.8.0
|
||||
"@httptoolkit/httpolyglot": ^2.2.1
|
||||
"@httptoolkit/subscriptions-transport-ws": ^0.11.2
|
||||
"@httptoolkit/websocket-stream": ^6.0.1
|
||||
"@types/cors": ^2.8.6
|
||||
"@types/node": "*"
|
||||
async-mutex: ^0.5.0
|
||||
base64-arraybuffer: ^0.1.5
|
||||
body-parser: ^1.15.2
|
||||
cacheable-lookup: ^6.0.0
|
||||
common-tags: ^1.8.0
|
||||
connect: ^3.7.0
|
||||
cors: ^2.8.4
|
||||
cors-gate: ^1.1.3
|
||||
cross-fetch: ^3.1.5
|
||||
destroyable-server: ^1.0.2
|
||||
express: ^4.14.0
|
||||
fast-json-patch: ^3.1.1
|
||||
graphql: ^14.0.2 || ^15.5
|
||||
graphql-http: ^1.22.0
|
||||
graphql-subscriptions: ^1.1.0
|
||||
graphql-tag: ^2.12.6
|
||||
http-encoding: ^2.0.1
|
||||
http2-wrapper: ^2.2.1
|
||||
https-proxy-agent: ^5.0.1
|
||||
isomorphic-ws: ^4.0.1
|
||||
lodash: ^4.16.4
|
||||
lru-cache: ^7.14.0
|
||||
native-duplexpair: ^1.0.0
|
||||
node-forge: ^1.2.1
|
||||
pac-proxy-agent: ^7.0.0
|
||||
parse-multipart-data: ^1.4.0
|
||||
performance-now: ^2.1.0
|
||||
portfinder: ^1.0.32
|
||||
read-tls-client-hello: ^1.0.0
|
||||
semver: ^7.5.3
|
||||
socks-proxy-agent: ^7.0.0
|
||||
typed-error: ^3.0.2
|
||||
urlpattern-polyfill: ^8.0.0
|
||||
uuid: ^8.3.2
|
||||
ws: ^8.8.0
|
||||
bin:
|
||||
mockttp: dist/admin/admin-bin.js
|
||||
checksum: 96b90e0515e7ac1b73954e9e01010424d51d9563f8e850e620b06ba864bf064401e1a1af89e103724b956bfa3bee790cb452366df100bf01122f333e04c3aee8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mockttp@npm:^3.9.1":
|
||||
version: 3.9.4
|
||||
resolution: "mockttp@npm:3.9.4"
|
||||
@@ -43704,6 +43814,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"urlpattern-polyfill@npm:^8.0.0":
|
||||
version: 8.0.2
|
||||
resolution: "urlpattern-polyfill@npm:8.0.2"
|
||||
checksum: d2cc0905a613c77e330c426e8697ee522dd9640eda79ac51160a0f6350e103f09b8c327623880989f8ba7325e8d95267b745aa280fdcc2aead80b023e16bd09d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"urlpattern-polyfill@npm:^9.0.0":
|
||||
version: 9.0.0
|
||||
resolution: "urlpattern-polyfill@npm:9.0.0"
|
||||
@@ -45259,6 +45376,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zstd-codec@npm:^0.1.5":
|
||||
version: 0.1.5
|
||||
resolution: "zstd-codec@npm:0.1.5"
|
||||
checksum: ba62bf643c3ca9759fedc090b73a0c3b1e506364fcae902a70b112c1f5b30bc6aabff3184808cc4430f2ab6644cabae979368152ae908c1d8ef39cd8c3223c85
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zwitch@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "zwitch@npm:2.0.2"
|
||||
|
||||
Reference in New Issue
Block a user