adding test for request and response validation and adjusting error messages

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2024-07-07 14:50:12 -04:00
parent 3bf5285bd6
commit ea0b6b4262
12 changed files with 493 additions and 104 deletions
@@ -0,0 +1,29 @@
{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {
"/api/search": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"query": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
@@ -23,7 +23,8 @@
}
}
}
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,25 @@
{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0.0" },
"paths": {
"/api/item/{id}": {
"get": {
"responses": {
"200": {
"description": "OK"
}
},
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
]
}
}
}
}
@@ -18,6 +18,20 @@ import { Operation } from './types';
export class OperationError extends Error {
constructor(operation: Operation, message: string) {
super(`[${operation.path} (${operation.method})]: ${message}`);
super(
`["${operation.method.toLocaleUpperCase('en-US')} ${
operation.path
}"] ${message}`,
);
}
}
export class OperationResponseError extends Error {
constructor(operation: Operation, response: Response, message: string) {
super(
`["${operation.method.toLocaleUpperCase('en-US')} ${operation.path}" (${
response.status
})]: ${message}`,
);
}
}
@@ -16,7 +16,11 @@
import _ from 'lodash';
import withQueryParameter from './__fixtures__/schemas/withQueryParameter.json';
import { QueryParameterParser } from './parameter-validation';
import withPathParameter from './__fixtures__/schemas/withPathParameter.json';
import {
PathParameterParser,
QueryParameterParser,
} from './parameter-validation';
import { OperationObject, ParameterObject } from 'openapi3-ts';
import Ajv from 'ajv';
import { Operation } from './types';
@@ -54,7 +58,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
@@ -66,7 +70,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
});
@@ -90,7 +94,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Query parameter validation failed"`,
`"["GET /api/search"] Query parameter validation failed"`,
);
});
});
@@ -132,7 +136,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -143,7 +147,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
});
@@ -177,7 +181,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -188,7 +192,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
});
@@ -215,7 +219,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -226,7 +230,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
});
@@ -252,7 +256,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -263,7 +267,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
});
@@ -304,7 +308,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -361,7 +365,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -372,7 +376,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
});
@@ -416,7 +420,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Required query parameter param not found"`,
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -427,7 +431,7 @@ describe('query parameters', () => {
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (get)]: Unexpected query parameters: extra"`,
`"["GET /api/search"] Unexpected query parameters: extra"`,
);
});
@@ -456,3 +460,66 @@ describe('query parameters', () => {
});
});
});
describe('path parameters', () => {
let operation: Operation;
let parser: PathParameterParser;
let schema: (typeof withPathParameter)['paths']['/api/item/{id}']['get'];
beforeEach(() => {
schema = _.cloneDeep(withPathParameter.paths['/api/item/{id}'].get);
operation = {
schema: schema as OperationObject,
path: '/api/item/{id}',
method: 'get',
};
parser = new PathParameterParser(operation, { ajv });
});
describe('primitives', () => {
describe('string', () => {
it('should parse a string', async () => {
const request = {
url: 'http://localhost:8080/api/item/test',
} as Request;
const result = await parser.parse(request);
expect(result.id).toBe('test');
});
it('should throw an error if the parameter is required but missing', async () => {
(schema.parameters![0] as ParameterObject).required = true;
const request = {
url: 'http://localhost:8080/api/item',
} as Request;
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/item/{id}"] Path parts do not match"`,
);
});
});
describe('number', () => {
beforeEach(() => {
schema.parameters![0].schema.type = 'number';
});
it('should parse a number', async () => {
const request = {
url: 'http://localhost:8080/api/item/42',
} as Request;
const result = await parser.parse(request);
expect(result.id).toBe(42);
});
it('should throw an error if the parameter is not a number', async () => {
const request = {
url: 'http://localhost:8080/api/item/hello',
} as Request;
await expect(
parser.parse(request),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/item/{id}"] Path parameter validation failed"`,
);
});
});
});
});
@@ -36,7 +36,11 @@ class BaseParameterParser {
ajv: Ajv;
operation: Operation;
parameters: Record<string, ReferencelessParameterObject> = {};
constructor(operation: Operation, options: ParserOptions) {
constructor(
parameterIn: string,
operation: Operation,
options: ParserOptions,
) {
this.ajv = options.ajv;
this.operation = operation;
const { schema, path, method } = operation;
@@ -60,18 +64,31 @@ class BaseParameterParser {
'Reference objects are not supported for parameters',
);
}
if (parameter.in === 'query') {
if (parameter.in === parameterIn) {
this.parameters[parameter.name] =
parameter as ReferencelessParameterObject;
}
}
}
optimisticallyParseValue(value: string, schema: SchemaObject) {
if (schema.type === 'integer') {
return parseInt(value, 10);
}
if (schema.type === 'number') {
return parseFloat(value);
}
return value;
}
}
export class QueryParameterParser
extends BaseParameterParser
implements RequestParser<Record<string, any>>
{
constructor(operation: Operation, options: ParserOptions) {
super('query', operation, options);
}
async parse(request: Request) {
const { searchParams } = new URL(request.url);
const remainingQueryParameters = new Set<string>(searchParams.keys());
@@ -127,13 +144,8 @@ export class QueryParameterParser
} 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);
}
if (parameter.schema.type === 'number') {
// Try to parse the number as AJV won't do it for us.
param = parseFloat(param);
if (param) {
param = this.optimisticallyParseValue(param, parameter.schema);
}
const validate = this.ajv.compile(parameter.schema);
const valid = validate(param);
@@ -295,6 +307,9 @@ export class HeaderParameterParser
extends BaseParameterParser
implements RequestParser<Record<string, any>>
{
constructor(operation: Operation, options: ParserOptions) {
super('header', operation, options);
}
async parse(request: Request) {
const headerParameters: Record<string, any> = {};
for (const [name, parameter] of Object.entries(this.parameters)) {
@@ -339,15 +354,19 @@ export class PathParameterParser
extends BaseParameterParser
implements RequestParser<Record<string, any>>
{
constructor(operation: Operation, options: ParserOptions) {
super('path', operation, options);
}
async parse(request: Request) {
const { pathname } = new URL(request.url);
const params = this.parsePath({
path: pathname,
schema: this.operation.path,
});
const pathParameters: Record<string, string> = {};
const pathParameters: Record<string, any> = {};
for (const [name, parameter] of Object.entries(this.parameters)) {
if (!params[name] && parameter.required) {
let param: string | number = params[name];
if (!param && parameter.required) {
throw new OperationError(
this.operation,
`Path parameter ${name} not found`,
@@ -356,8 +375,12 @@ export class PathParameterParser
continue;
}
if (param) {
param = this.optimisticallyParseValue(param, parameter.schema);
}
const validate = this.ajv.compile(parameter.schema);
const valid = validate(params[name]);
const valid = validate(param);
if (!valid) {
throw new OperationError(
@@ -365,7 +388,7 @@ export class PathParameterParser
'Path parameter validation failed',
);
}
pathParameters[name] = params[name];
pathParameters[name] = param;
}
return pathParameters;
}
@@ -0,0 +1,82 @@
/*
* 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 withJsonRequestBody from './__fixtures__/schemas/withJsonRequestBody.json';
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 { JsonObject } from '@backstage/types';
const ajv = new Ajv();
function toRequest(body?: object, headers?: Record<string, string>): Request {
return {
text: async () => JSON.stringify(body),
json: async () => body,
url: '/api/search',
method: 'post',
headers: new Headers({ 'content-type': 'application/json', ...headers }),
} as Request;
}
describe('request body', () => {
let operation: Operation;
let parser: RequestParser<JsonObject | undefined>;
let schema: (typeof withJsonRequestBody)['paths']['/api/search']['post'];
beforeEach(() => {
schema = _.cloneDeep(withJsonRequestBody.paths['/api/search'].post);
operation = {
method: 'post',
schema: schema as OperationObject,
path: '/api/search',
};
parser = new RequestBodyParser(operation, {
ajv,
});
});
it('should validate request body', async () => {
const requestBody = {
query: 'test',
};
const result = await parser.parse(toRequest(requestBody));
expect(result).toEqual(requestBody);
});
it('should throw error if request body is not valid', async () => {
const requestBody = {
query: 1,
};
await expect(
parser.parse(toRequest(requestBody)),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["POST /api/search"] Request body validation failed."`,
);
});
it('should throw error if request body is required but missing', async () => {
(schema.requestBody as RequestBodyObject).required = true;
parser = RequestBodyParser.fromOperation(operation, {
ajv,
});
await expect(
parser.parse(toRequest()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["POST /api/search"] No request body found for /api/search"`,
);
});
});
@@ -18,24 +18,51 @@ import { JsonObject } from '@backstage/types';
import { Operation, ParserOptions, RequestParser } from './types';
import { ValidateFunction } from 'ajv';
import { OperationError } from './errors';
import { RequestBodyObject, SchemaObject } from 'openapi3-ts';
class DisabledRequestBodyParser
implements RequestParser<JsonObject | undefined>
{
operation: Operation;
constructor(operation: Operation) {
this.operation = operation;
}
async parse(request: Request): Promise<JsonObject | undefined> {
const bodyText = await request.text();
if (bodyText?.length) {
throw new OperationError(
this.operation,
'Received a body but no schema was found',
);
}
return undefined;
}
}
export class RequestBodyParser
implements RequestParser<JsonObject | undefined>
{
operation: Operation;
validate:
| { fn: ValidateFunction; disabled: false }
| {
fn: undefined;
disabled: true;
};
disabled: boolean = false;
validate!: ValidateFunction;
schema!: SchemaObject;
requestBodySchema!: RequestBodyObject;
static fromOperation(operation: Operation, options: ParserOptions) {
return operation.schema.requestBody
? new RequestBodyParser(operation, options)
: new DisabledRequestBodyParser(operation);
}
constructor(operation: Operation, options: ParserOptions) {
this.operation = operation;
const { schema: operationSchema } = this.operation;
const requestBody = operationSchema.requestBody;
if (!requestBody) {
this.validate = { disabled: true, fn: undefined };
return;
throw new OperationError(
this.operation,
'No request body found in operation',
);
}
if ('$ref' in requestBody!) {
@@ -69,27 +96,17 @@ export class RequestBodyParser
'Reference objects are not supported',
);
}
this.validate = {
disabled: false,
fn: options.ajv.compile(operation.schema),
};
this.validate = options.ajv.compile(schema);
this.schema = schema;
this.requestBodySchema = requestBody;
}
async parse(request: Request): Promise<JsonObject | undefined> {
const { disabled, fn } = this.validate;
const bodyText = await request.text();
if (!disabled && bodyText?.length) {
if (this.requestBodySchema.required && !bodyText?.length) {
throw new OperationError(
this.operation,
`No request body found for ${request.url}`,
);
} else if (disabled && !bodyText?.length) {
// If there is no request body in the schema and no body in the request, then the request is valid
return undefined;
} else if (disabled && bodyText?.length) {
throw new OperationError(
this.operation,
'Received a body but no schema was found',
);
}
const contentType =
@@ -101,10 +118,8 @@ export class RequestBodyParser
);
}
const body = (await request.json()) as JsonObject;
const valid = fn!(body);
const valid = this.validate(body);
if (!valid) {
console.log(body);
console.error(fn!.errors);
throw new OperationError(
this.operation,
`Request body validation failed.`,
@@ -0,0 +1,81 @@
/*
* 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 { JsonObject } from '@backstage/types';
import withJsonResponseBody from './__fixtures__/schemas/withJsonResponseBody.json';
import { Operation, ResponseParser } from './types';
import { ResponseBodyParser } from './response-body-validation';
import Ajv from 'ajv';
import { OperationObject, ResponsesObject } from 'openapi3-ts';
import _ from 'lodash';
const ajv = new Ajv();
function toResponse(body?: object): Response {
return {
json: async () => body,
text: async () => JSON.stringify(body),
status: 200,
} as Response;
}
describe('response body', () => {
let operation: Operation;
let parser: ResponseParser<JsonObject | undefined>;
let schema: (typeof withJsonResponseBody)['paths']['/api/search']['get'];
beforeEach(() => {
schema = _.cloneDeep(withJsonResponseBody.paths['/api/search'].get);
operation = {
path: '/api/search',
method: 'get',
schema: schema as OperationObject,
};
parser = ResponseBodyParser.fromOperation(operation, {
ajv,
});
});
it('should validate response body', async () => {
const responseBody = {
results: [{ id: 'test' }],
};
const result = await parser.parse(toResponse(responseBody));
expect(result).toEqual(responseBody);
});
it('should throw error if response body is not valid', async () => {
const responseBody = {
result: 1,
};
await expect(
parser.parse(toResponse(responseBody)),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search" (200)]: Response body validation failed"`,
);
});
it('should throw error if response body is required but missing', async () => {
(schema.responses as ResponsesObject)['200'].required = true;
parser = ResponseBodyParser.fromOperation(operation, {
ajv,
});
await expect(
parser.parse(toResponse()),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search" (200)]: Response body is required but missing"`,
);
});
});
@@ -16,22 +16,46 @@
import { JsonObject } from '@backstage/types';
import { Operation, ParserOptions, ResponseParser } from './types';
import { OperationError } from './errors';
import { OperationError, OperationResponseError } from './errors';
import Ajv from 'ajv';
import { OperationObject, ResponseObject } from 'openapi3-ts';
class DisabledResponseBodyParser
implements ResponseParser<JsonObject | undefined>
{
operation: Operation;
constructor(operation: Operation) {
this.operation = operation;
}
async parse(response: Response): Promise<JsonObject | undefined> {
const body = await response.text();
if (body?.length) {
throw new OperationError(
this.operation,
'Received a body but no schema was found',
);
}
return undefined;
}
}
export class ResponseBodyParser
implements ResponseParser<JsonObject | undefined>
{
operation: Operation;
ajv: Ajv;
static fromOperation(operation: Operation, options: ParserOptions) {
return operation.schema.responses &&
Object.keys(operation.schema.responses).length
? new ResponseBodyParser(operation, options)
: new DisabledResponseBodyParser(operation);
}
constructor(operation: Operation, options: ParserOptions) {
this.operation = operation;
this.ajv = options.ajv;
const responseSchemas = operation.schema.responses;
if (!Object.keys(responseSchemas).length) {
throw new OperationError(this.operation, `No response schemas found`);
}
for (const [statusCode, schema] of Object.entries(responseSchemas)) {
if (!schema.content) {
continue;
@@ -55,15 +79,30 @@ export class ResponseBodyParser
this.operation.schema,
response,
);
if (!responseSchema?.content && body?.length) {
throw new OperationError(this.operation, '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
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.
// eg 204 No Content
return undefined;
}
if (!responseSchema?.content!['application/json']) {
throw new OperationError(
if (!responseSchema) {
throw new OperationResponseError(
this.operation,
response,
`No schema found.`,
);
}
if (!responseSchema?.content && body?.length) {
throw new OperationResponseError(
this.operation,
response,
'Received a body but no schema was found',
);
}
if (!responseSchema?.content!['application/json']) {
throw new OperationResponseError(
this.operation,
response,
'No application/json content type found in response',
);
}
@@ -72,18 +111,31 @@ export class ResponseBodyParser
throw new OperationError(this.operation, 'No schema found in response');
}
if ('$ref' in schema) {
throw new OperationError(
throw new OperationResponseError(
this.operation,
response,
'Reference objects are not supported',
);
}
if (!schema.required && !body?.length) {
throw new OperationResponseError(
this.operation,
response,
'Response body is required but missing',
);
} else if (!schema.required && !body?.length) {
// If there is no content in the response schema and no body in the response, then the response is valid
return undefined;
}
const validate = this.ajv.compile(schema);
const jsonBody = (await response.json()) as JsonObject;
const valid = validate(jsonBody);
if (!valid) {
throw new OperationError(
throw new OperationResponseError(
this.operation,
response,
'Response body validation failed',
);
}
@@ -112,8 +112,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Received a body but no schema was found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Received a body but no schema was found"`,
);
});
});
@@ -149,8 +149,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Unexpected query parameters: test]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Unexpected query parameters: test"`,
);
});
@@ -166,8 +166,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Query parameter validation failed]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Query parameter validation failed"`,
);
});
@@ -205,8 +205,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Unexpected query parameters: param]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Unexpected query parameters: param"`,
);
});
@@ -224,8 +224,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -241,8 +241,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Invalid object parameter]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Invalid object parameter"`,
);
});
});
@@ -265,8 +265,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Unexpected query parameters: param[t]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Unexpected query parameters: param[t"`,
);
});
@@ -284,8 +284,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -300,8 +300,8 @@ describe('OpenApiProxyValidator', () => {
});
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Invalid object parameter]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Invalid object parameter"`,
);
});
describe('explode', () => {
@@ -369,7 +369,7 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"[/api/search (GET)]: Invalid object parameter"`,
`"["GET /api/search"] Invalid object parameter"`,
);
});
});
@@ -428,8 +428,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -445,8 +445,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Unexpected query parameters: param[]]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Unexpected query parameters: param[]"`,
);
});
});
@@ -499,8 +499,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
@@ -516,8 +516,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Array parameter should not have multiple values]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Array parameter should not have multiple values"`,
);
});
});
@@ -609,8 +609,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
});
@@ -662,8 +662,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Required query parameter param not found]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search"] Required query parameter param not found"`,
);
});
});
@@ -703,8 +703,8 @@ describe('OpenApiProxyValidator', () => {
await expect(
async () => await validator.validate(request, response),
).rejects.toMatchInlineSnapshot(
`[Error: [/api/search (GET)]: Response body validation failed]`,
).rejects.toThrowErrorMatchingInlineSnapshot(
`"["GET /api/search" (200)]: Response body is required but missing"`,
);
});
});
@@ -40,7 +40,7 @@ class RequestBodyValidator implements Validator {
async validate({ pair, operation }: ValidatorParams) {
const { request } = pair;
const parser = new RequestBodyParser(operation, { ajv });
const parser = RequestBodyParser.fromOperation(operation, { ajv });
const fetchRequest = mockttpToFetchRequest(request);
await parser.parse(fetchRequest);
}
@@ -54,7 +54,7 @@ class ResponseBodyValidator implements Validator {
async validate({ pair, operation }: ValidatorParams) {
const { response } = pair;
const parser = new ResponseBodyParser(operation, { ajv });
const parser = ResponseBodyParser.fromOperation(operation, { ajv });
const fetchResponse = mockttpToFetchResponse(response);
await parser.parse(fetchResponse);
}