Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,80 @@ describe('OpenAPIBackend', () => {
expect(mockHandler).toBeCalled();
});
});

describe('validation option', () => {
const validationDefinition: OpenAPIV3_1.Document = {
...meta,
paths: {
'/pets/{id}': {
get: {
operationId: 'getPetById',
responses,
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: {
type: 'integer',
},
},
],
},
},
},
};

test('uses validate predicate and skips validation when predicate returns false', async () => {
let seenContext: Context | undefined;
let seenArg: string | undefined;
const validate = jest.fn((context: Context, arg: string) => {
seenContext = context;
seenArg = arg;
return false;
});
const api = new OpenAPIBackend({ definition: validationDefinition, validate });
const operationHandler = jest.fn(() => 'operation-response');
const validationFailHandler = jest.fn(() => 'validation-failed');
api.register('getPetById', operationHandler);
api.register('validationFail', validationFailHandler);
await api.init();

const request = {
method: 'get',
path: '/pets/not-an-integer',
headers: {},
};
const res = await api.handleRequest(request, 'handler-arg');

expect(validate).toBeCalledTimes(1);
expect(seenContext?.operation.operationId).toBe('getPetById');
expect(seenArg).toBe('handler-arg');
expect(validationFailHandler).not.toBeCalled();
expect(operationHandler).toBeCalledTimes(1);
expect(res).toBe('operation-response');
});

test('runs validation when validate predicate returns true', async () => {
const api = new OpenAPIBackend({ definition: validationDefinition, validate: () => true });
const operationHandler = jest.fn(() => 'operation-response');
const validationFailHandler = jest.fn(() => 'validation-failed');
api.register('getPetById', operationHandler);
api.register('validationFail', validationFailHandler);
await api.init();

const request = {
method: 'get',
path: '/pets/not-an-integer',
headers: {},
};
const res = await api.handleRequest(request);

expect(validationFailHandler).toBeCalledTimes(1);
expect(operationHandler).not.toBeCalled();
expect(res).toBe('validation-failed');
});
});
});

describe('types coercion', () => {
Expand Down
14 changes: 9 additions & 5 deletions src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export type Handler<
*/
export type HandlerMap = { [operationId: string]: Handler | undefined };

export type BoolPredicate = (context: Context, ...args: any[]) => boolean;
export type ContextPredicate = (context: Context, ...args: any[]) => boolean;
/**
* @deprecated Use ContextPredicate instead.
*/
export type BoolPredicate = ContextPredicate;

/**
* The different possibilities for set matching.
Expand All @@ -92,7 +96,7 @@ export interface Options<D extends Document = Document> {
apiRoot?: string;
strict?: boolean;
quick?: boolean;
validate?: boolean | BoolPredicate;
validate?: boolean | ContextPredicate;
ajvOpts?: AjvOpts;
customizeAjv?: AjvCustomizer;
handlers?: HandlerMap & {
Expand Down Expand Up @@ -121,7 +125,7 @@ export class OpenAPIBackend<D extends Document = Document> {

public strict: boolean;
public quick: boolean;
public validate: boolean | BoolPredicate;
public validate: boolean | ContextPredicate;
public ignoreTrailingSlashes: boolean;

public ajvOpts: AjvOpts;
Expand Down Expand Up @@ -159,7 +163,7 @@ export class OpenAPIBackend<D extends Document = Document> {
* @param {string} opts.apiRoot - the root URI of the api. all paths are matched relative to apiRoot
* @param {boolean} opts.strict - strict mode, throw errors or warn on OpenAPI spec validation errors (default: false)
* @param {boolean} opts.quick - quick startup, attempts to optimise startup; might break things (default: false)
* @param {boolean} opts.validate - whether to validate requests with Ajv (default: true)
* @param {boolean | ContextPredicate} opts.validate - whether to validate requests with Ajv (default: true)
* @param {boolean} opts.ignoreTrailingSlashes - whether to ignore trailing slashes when routing (default: true)
* @param {boolean} opts.ajvOpts - default ajv opts to pass to the validator
* @param {boolean} opts.coerceTypes - enable coerce typing of request path and query parameters. Requires validate to be enabled. (default: false)
Expand All @@ -182,7 +186,7 @@ export class OpenAPIBackend<D extends Document = Document> {
this.inputDocument = optsWithDefaults.definition;
this.strict = !!optsWithDefaults.strict;
this.quick = !!optsWithDefaults.quick;
this.validate = !!optsWithDefaults.validate;
this.validate = optsWithDefaults.validate ?? true;
this.ignoreTrailingSlashes = !!optsWithDefaults.ignoreTrailingSlashes;
this.handlers = { ...optsWithDefaults.handlers }; // Copy to avoid mutating passed object
this.securityHandlers = { ...optsWithDefaults.securityHandlers }; // Copy to avoid mutating passed object
Expand Down