diff --git a/src/backend.test.ts b/src/backend.test.ts index 17fefab3..71d8145c 100644 --- a/src/backend.test.ts +++ b/src/backend.test.ts @@ -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', () => { diff --git a/src/backend.ts b/src/backend.ts index 39437395..ce15d3fc 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -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. @@ -92,7 +96,7 @@ export interface Options { apiRoot?: string; strict?: boolean; quick?: boolean; - validate?: boolean | BoolPredicate; + validate?: boolean | ContextPredicate; ajvOpts?: AjvOpts; customizeAjv?: AjvCustomizer; handlers?: HandlerMap & { @@ -121,7 +125,7 @@ export class OpenAPIBackend { public strict: boolean; public quick: boolean; - public validate: boolean | BoolPredicate; + public validate: boolean | ContextPredicate; public ignoreTrailingSlashes: boolean; public ajvOpts: AjvOpts; @@ -159,7 +163,7 @@ export class OpenAPIBackend { * @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) @@ -182,7 +186,7 @@ export class OpenAPIBackend { 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