feat(openapi-tooling): custom validation server for test cases

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2024-07-05 17:13:56 -04:00
parent 0d74ac9e71
commit 5d8114c1d6
10 changed files with 810 additions and 10 deletions
@@ -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"
},
+1 -1
View File
@@ -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(() => {
+125 -1
View File
@@ -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"