From 9af077a058c89a5fcf9454893b08071bba89c948 Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:25:09 -0700 Subject: [PATCH 1/8] feat(validation): add @validator decorator for JSON Schema validation --- packages/validation/src/decorator.ts | 79 ++++++++++++++++ .../validation/tests/unit/decorator.test.ts | 90 +++++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 packages/validation/src/decorator.ts create mode 100644 packages/validation/tests/unit/decorator.test.ts diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts new file mode 100644 index 0000000000..d92c8bf407 --- /dev/null +++ b/packages/validation/src/decorator.ts @@ -0,0 +1,79 @@ +import type { Ajv } from 'ajv'; +import { SchemaValidationError } from './errors.js'; +import { validate } from './validate.js'; +export interface ValidatorOptions { + inboundSchema?: object; + outboundSchema?: object; + envelope?: string; + formats?: Record< + string, + | string + | RegExp + | { + type?: 'string' | 'number'; + validate: (data: string) => boolean; + async?: boolean; + } + >; + externalRefs?: object[]; + ajv?: Ajv; +} + +type AsyncMethod = (...args: unknown[]) => Promise; + +export function validator(options: ValidatorOptions): MethodDecorator { + return ( + _target, + _propertyKey, + descriptor: TypedPropertyDescriptor + ) => { + if (!descriptor.value) { + return descriptor; + } + + if (!options.inboundSchema && !options.outboundSchema) { + return descriptor; + } + + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: unknown[]): Promise { + let validatedInput = args[0]; + + if (options.inboundSchema) { + try { + validatedInput = validate({ + payload: args[0], + schema: options.inboundSchema, + envelope: options.envelope, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Inbound validation failed', error); + } + } + + const result = await originalMethod.apply(this, [ + validatedInput, + ...args.slice(1), + ]); + if (options.outboundSchema) { + try { + return validate({ + payload: result, + schema: options.outboundSchema, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Outbound Validation failed', error); + } + } + return result; + }; + return descriptor; + }; +} diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts new file mode 100644 index 0000000000..3af81ee76f --- /dev/null +++ b/packages/validation/tests/unit/decorator.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; +import { validator } from '../../src/decorator.js'; +import { SchemaValidationError } from '../../src/errors.js'; + +const inboundSchema = { + type: 'object', + properties: { + value: { type: 'number' }, + }, + required: ['value'], + additionalProperties: false, +}; + +const outboundSchema = { + type: 'object', + properties: { + result: { type: 'number' }, + }, + required: ['result'], + additionalProperties: false, +}; + +class TestClass { + @validator({ inboundSchema, outboundSchema }) + async multiply(input: { value: number }): Promise<{ result: number }> { + return { result: input.value * 2 }; + } +} + +describe('validator decorator', () => { + it('should validate inbound and outbound successfully', async () => { + // Prepare + const instance = new TestClass(); + const input = { value: 5 }; + + // Act + const output = await instance.multiply(input); + + // Assess + expect(output).toEqual({ result: 10 }); + }); + + it('should throw error on inbound validation failure', async () => { + // Prepare + const instance = new TestClass(); + const invalidInput = { value: 'not a number' } as unknown as { + value: number; + }; + + // Act & Assess + await expect(instance.multiply(invalidInput)).rejects.toThrow( + SchemaValidationError + ); + }); + + it('should throw error on outbound validation failure', async () => { + // Prepare + class TestClassInvalid { + @validator({ inboundSchema, outboundSchema }) + async multiply(input: { value: number }): Promise<{ result: number }> { + return { result: 'invalid' } as unknown as { result: number }; + } + } + const instance = new TestClassInvalid(); + const input = { value: 5 }; + + // Act & Assess + await expect(instance.multiply(input)).rejects.toThrow( + SchemaValidationError + ); + }); + + it('should no-op when no schemas are provided', async () => { + // Prepare + class TestClassNoOp { + @validator({}) + async echo(input: unknown): Promise { + return input; + } + } + const instance = new TestClassNoOp(); + const data = { foo: 'bar' }; + + // Act + const result = await instance.echo(data); + + // Assess + expect(result).toEqual(data); + }); +}); From 4e0ea04414a747fb3f2adfa087937ca7716349ca Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:36:53 -0700 Subject: [PATCH 2/8] Updated the test suite --- .../validation/tests/unit/decorator.test.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts index 3af81ee76f..127f1db1c8 100644 --- a/packages/validation/tests/unit/decorator.test.ts +++ b/packages/validation/tests/unit/decorator.test.ts @@ -20,33 +20,35 @@ const outboundSchema = { additionalProperties: false, }; -class TestClass { - @validator({ inboundSchema, outboundSchema }) - async multiply(input: { value: number }): Promise<{ result: number }> { - return { result: input.value * 2 }; - } -} - describe('validator decorator', () => { it('should validate inbound and outbound successfully', async () => { // Prepare + class TestClass { + @validator({ inboundSchema, outboundSchema }) + async multiply(input: { value: number }): Promise<{ result: number }> { + return { result: input.value * 2 }; + } + } const instance = new TestClass(); const input = { value: 5 }; - // Act const output = await instance.multiply(input); - // Assess expect(output).toEqual({ result: 10 }); }); it('should throw error on inbound validation failure', async () => { // Prepare + class TestClass { + @validator({ inboundSchema, outboundSchema }) + async multiply(input: { value: number }): Promise<{ result: number }> { + return { result: input.value * 2 }; + } + } const instance = new TestClass(); const invalidInput = { value: 'not a number' } as unknown as { value: number; }; - // Act & Assess await expect(instance.multiply(invalidInput)).rejects.toThrow( SchemaValidationError @@ -63,7 +65,6 @@ describe('validator decorator', () => { } const instance = new TestClassInvalid(); const input = { value: 5 }; - // Act & Assess await expect(instance.multiply(input)).rejects.toThrow( SchemaValidationError @@ -80,11 +81,22 @@ describe('validator decorator', () => { } const instance = new TestClassNoOp(); const data = { foo: 'bar' }; - // Act const result = await instance.echo(data); - // Assess expect(result).toEqual(data); }); + + it('should return descriptor unmodified if descriptor.value is undefined', () => { + // Prepare + const descriptor: PropertyDescriptor = {}; + // Act + const result = validator({ inboundSchema })( + null as unknown, + 'testMethod', + descriptor + ); + // Assess + expect(result).toEqual(descriptor); + }); }); From 4984867d3e8f94346b0362928fc6b064cdbc1ea4 Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:39:08 -0700 Subject: [PATCH 3/8] updated test suite --- .../validation/tests/unit/decorator.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts index 127f1db1c8..8e0f5a06d3 100644 --- a/packages/validation/tests/unit/decorator.test.ts +++ b/packages/validation/tests/unit/decorator.test.ts @@ -99,4 +99,36 @@ describe('validator decorator', () => { // Assess expect(result).toEqual(descriptor); }); + + it('should validate inbound only', async () => { + // Prepare + class TestClassInbound { + @validator({ inboundSchema }) + async process(input: { value: number }): Promise<{ data: string }> { + return { data: JSON.stringify(input) }; + } + } + const instance = new TestClassInbound(); + const input = { value: 10 }; + // Act + const output = await instance.process(input); + // Assess + expect(output).toEqual({ data: JSON.stringify(input) }); + }); + + it('should validate outbound only', async () => { + // Prepare + class TestClassOutbound { + @validator({ outboundSchema }) + async process(input: { text: string }): Promise<{ result: number }> { + return { result: 42 }; + } + } + const instance = new TestClassOutbound(); + const input = { text: 'hello' }; + // Act + const output = await instance.process(input); + // Assess + expect(output).toEqual({ result: 42 }); + }); }); From a4f1060838e218d4395eb2b3245c56aa31ff89fb Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:42:10 -0700 Subject: [PATCH 4/8] refactor(validation): update decorator with improved types and schema validation --- packages/validation/src/decorator.ts | 26 ++------------ packages/validation/src/types.ts | 36 ++++++++++++++----- .../validation/tests/unit/decorator.test.ts | 2 +- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts index d92c8bf407..6232542ed7 100644 --- a/packages/validation/src/decorator.ts +++ b/packages/validation/src/decorator.ts @@ -1,23 +1,6 @@ -import type { Ajv } from 'ajv'; import { SchemaValidationError } from './errors.js'; +import type { ValidatorOptions } from './types.js'; import { validate } from './validate.js'; -export interface ValidatorOptions { - inboundSchema?: object; - outboundSchema?: object; - envelope?: string; - formats?: Record< - string, - | string - | RegExp - | { - type?: 'string' | 'number'; - validate: (data: string) => boolean; - async?: boolean; - } - >; - externalRefs?: object[]; - ajv?: Ajv; -} type AsyncMethod = (...args: unknown[]) => Promise; @@ -30,16 +13,12 @@ export function validator(options: ValidatorOptions): MethodDecorator { if (!descriptor.value) { return descriptor; } - if (!options.inboundSchema && !options.outboundSchema) { return descriptor; } - const originalMethod = descriptor.value; - descriptor.value = async function (...args: unknown[]): Promise { let validatedInput = args[0]; - if (options.inboundSchema) { try { validatedInput = validate({ @@ -54,7 +33,6 @@ export function validator(options: ValidatorOptions): MethodDecorator { throw new SchemaValidationError('Inbound validation failed', error); } } - const result = await originalMethod.apply(this, [ validatedInput, ...args.slice(1), @@ -69,7 +47,7 @@ export function validator(options: ValidatorOptions): MethodDecorator { ajv: options.ajv, }); } catch (error) { - throw new SchemaValidationError('Outbound Validation failed', error); + throw new SchemaValidationError('Outbound validation failed', error); } } return result; diff --git a/packages/validation/src/types.ts b/packages/validation/src/types.ts index fd1efbaab0..4543e6ffe9 100644 --- a/packages/validation/src/types.ts +++ b/packages/validation/src/types.ts @@ -1,18 +1,36 @@ -import type Ajv from 'ajv'; -export interface ValidateParams { +import type { + Ajv, + AnySchema, + AsyncFormatDefinition, + FormatDefinition, +} from 'ajv'; + +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +type ValidateParams = { payload: unknown; - schema: object; + schema: AnySchema; envelope?: string; formats?: Record< string, | string | RegExp - | { - type?: 'string' | 'number'; - validate: (data: string) => boolean; - async?: boolean; - } + | FormatDefinition + | FormatDefinition + | AsyncFormatDefinition + | AsyncFormatDefinition >; externalRefs?: object[]; ajv?: Ajv; -} +}; + +type ValidatorOptions = Prettify< + Omit & { + inboundSchema?: AnySchema; + outboundSchema?: AnySchema; + } +>; + +export type { ValidateParams, ValidatorOptions }; diff --git a/packages/validation/tests/unit/decorator.test.ts b/packages/validation/tests/unit/decorator.test.ts index 8e0f5a06d3..94ba2f0c40 100644 --- a/packages/validation/tests/unit/decorator.test.ts +++ b/packages/validation/tests/unit/decorator.test.ts @@ -92,7 +92,7 @@ describe('validator decorator', () => { const descriptor: PropertyDescriptor = {}; // Act const result = validator({ inboundSchema })( - null as unknown, + null as unknown as object, 'testMethod', descriptor ); From 8676b00a9bf39a06247d4a6b4c862c1d6346898e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 4 Mar 2025 09:36:01 +0100 Subject: [PATCH 5/8] Update packages/validation/src/decorator.ts --- packages/validation/src/decorator.ts | 51 +++++++++++++++------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/validation/src/decorator.ts b/packages/validation/src/decorator.ts index 6232542ed7..b55e5159ad 100644 --- a/packages/validation/src/decorator.ts +++ b/packages/validation/src/decorator.ts @@ -1,33 +1,38 @@ import { SchemaValidationError } from './errors.js'; -import type { ValidatorOptions } from './types.js'; import { validate } from './validate.js'; - -type AsyncMethod = (...args: unknown[]) => Promise; - -export function validator(options: ValidatorOptions): MethodDecorator { +import type { ValidatorOptions } from './types.js'; +export function validator(options: ValidatorOptions) { return ( - _target, - _propertyKey, - descriptor: TypedPropertyDescriptor + _target: unknown, + _propertyKey: string | symbol, + descriptor: PropertyDescriptor ) => { if (!descriptor.value) { return descriptor; } - if (!options.inboundSchema && !options.outboundSchema) { + const { + inboundSchema, + outboundSchema, + envelope, + formats, + externalRefs, + ajv, + } = options; + if (!inboundSchema && !outboundSchema) { return descriptor; } const originalMethod = descriptor.value; - descriptor.value = async function (...args: unknown[]): Promise { + descriptor.value = async function (...args: unknown[]) { let validatedInput = args[0]; - if (options.inboundSchema) { + if (inboundSchema) { try { validatedInput = validate({ - payload: args[0], - schema: options.inboundSchema, - envelope: options.envelope, - formats: options.formats, - externalRefs: options.externalRefs, - ajv: options.ajv, + payload: validatedInput, + schema: inboundSchema, + envelope: envelope, + formats: formats, + externalRefs: externalRefs, + ajv: ajv, }); } catch (error) { throw new SchemaValidationError('Inbound validation failed', error); @@ -37,17 +42,17 @@ export function validator(options: ValidatorOptions): MethodDecorator { validatedInput, ...args.slice(1), ]); - if (options.outboundSchema) { + if (outboundSchema) { try { return validate({ payload: result, - schema: options.outboundSchema, - formats: options.formats, - externalRefs: options.externalRefs, - ajv: options.ajv, + schema: outboundSchema, + formats: formats, + externalRefs: externalRefs, + ajv: ajv, }); } catch (error) { - throw new SchemaValidationError('Outbound validation failed', error); + throw new SchemaValidationError('Outbound Validation failed', error); } } return result; From e93f513870f8c717415c15237e10f8053459a9d2 Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Tue, 4 Mar 2025 01:47:56 -0700 Subject: [PATCH 6/8] Updated imports and exports --- packages/validation/src/index.ts | 3 ++- packages/validation/src/validate.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 093ce43dd5..039a9236fa 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -1,2 +1,3 @@ -export { validate } from './validate'; +export { validate } from './validate.js'; export { SchemaValidationError } from './errors.js'; +export { validator } from './decorator.js'; diff --git a/packages/validation/src/validate.ts b/packages/validation/src/validate.ts index 75e2af96af..36d0fccb0a 100644 --- a/packages/validation/src/validate.ts +++ b/packages/validation/src/validate.ts @@ -1,9 +1,9 @@ import { search } from '@aws-lambda-powertools/jmespath'; -import Ajv, { type ValidateFunction } from 'ajv'; +import { Ajv, type ValidateFunction } from 'ajv'; import { SchemaValidationError } from './errors.js'; import type { ValidateParams } from './types.js'; -export function validate(params: ValidateParams): T { +export function validate(params: ValidateParams): T { const { payload, schema, envelope, formats, externalRefs, ajv } = params; const ajvInstance = ajv || new Ajv({ allErrors: true }); From 59e171c0389692774c6093994b97720e6ab69bb0 Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Tue, 4 Mar 2025 02:25:31 -0700 Subject: [PATCH 7/8] feat(validation): add Middy.js middleware for JSON Schema validation --- packages/validation/src/index.ts | 1 + packages/validation/src/middleware.ts | 42 +++++++++ .../validation/tests/unit/middleware.test.ts | 87 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 packages/validation/src/middleware.ts create mode 100644 packages/validation/tests/unit/middleware.test.ts diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 039a9236fa..4a3bfabd56 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -1,3 +1,4 @@ export { validate } from './validate.js'; export { SchemaValidationError } from './errors.js'; export { validator } from './decorator.js'; +export { validationMiddleware } from './middleware.js'; diff --git a/packages/validation/src/middleware.ts b/packages/validation/src/middleware.ts new file mode 100644 index 0000000000..cb7cc8c88c --- /dev/null +++ b/packages/validation/src/middleware.ts @@ -0,0 +1,42 @@ +import { SchemaValidationError } from './errors.js'; +import type { ValidatorOptions } from './types.js'; +import { validate } from './validate.js'; + +export function validationMiddleware(options: ValidatorOptions) { + if (!options.inboundSchema && !options.outboundSchema) { + return {}; + } + return { + before: async (handler: { event: unknown }) => { + if (options.inboundSchema) { + try { + handler.event = validate({ + payload: handler.event, + schema: options.inboundSchema, + envelope: options.envelope, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Inbound validation failed', error); + } + } + }, + after: async (handler: { response: unknown }) => { + if (options.outboundSchema) { + try { + handler.response = validate({ + payload: handler.response, + schema: options.outboundSchema, + formats: options.formats, + externalRefs: options.externalRefs, + ajv: options.ajv, + }); + } catch (error) { + throw new SchemaValidationError('Outbound validation failed', error); + } + } + }, + }; +} diff --git a/packages/validation/tests/unit/middleware.test.ts b/packages/validation/tests/unit/middleware.test.ts new file mode 100644 index 0000000000..415374e660 --- /dev/null +++ b/packages/validation/tests/unit/middleware.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; +import { SchemaValidationError } from '../../src/errors.js'; +import { validationMiddleware } from '../../src/middleware.js'; + +const inboundSchema = { + type: 'object', + properties: { + inputValue: { type: 'number' }, + }, + required: ['inputValue'], + additionalProperties: false, +}; + +const outboundSchema = { + type: 'object', + properties: { + outputValue: { type: 'number' }, + }, + required: ['outputValue'], + additionalProperties: false, +}; + +describe('validatorMiddleware', () => { + it('should validate inbound and outbound successfully', async () => { + // Prepare + const middleware = validationMiddleware({ inboundSchema, outboundSchema }); + const handler = { + event: { inputValue: 10 }, + response: { outputValue: 20 }, + }; + // Act + if (middleware.before) { + await middleware.before(handler); + } + if (middleware.after) { + await middleware.after(handler); + } + // Assess + expect(handler.event).toEqual({ inputValue: 10 }); + expect(handler.response).toEqual({ outputValue: 20 }); + }); + + it('should throw error on inbound validation failure', async () => { + // Prepare + const middleware = validationMiddleware({ inboundSchema }); + const handler = { + event: { inputValue: 'invalid' }, + response: {}, + }; + // Act & Assess + await expect(middleware.before?.(handler)).rejects.toThrow( + SchemaValidationError + ); + }); + + it('should throw error on outbound validation failure', async () => { + // Prepare + const middleware = validationMiddleware({ outboundSchema }); + const handler = { + event: {}, + response: { outputValue: 'invalid' }, + }; + // Act & Assess + await expect(middleware.after?.(handler)).rejects.toThrow( + SchemaValidationError + ); + }); + + it('should no-op when no schemas are provided', async () => { + // Prepare + const middleware = validationMiddleware({}); + const handler = { + event: { someKey: 'value' }, + response: { anotherKey: 'value' }, + }; + // Act + if (middleware.before) { + await middleware.before(handler); + } + if (middleware.after) { + await middleware.after(handler); + } + // Assess + expect(handler.event).toEqual({ someKey: 'value' }); + expect(handler.response).toEqual({ anotherKey: 'value' }); + }); +}); From 9086bd64b2fcb78703957747e31f0aa647f3c0c8 Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:57:40 -0700 Subject: [PATCH 8/8] feat(validation): add Middy.js middleware integration tests --- packages/validation/src/index.ts | 1 - packages/validation/src/middleware.ts | 5 +- .../validation/tests/unit/middleware.test.ts | 70 ++++++++----------- 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 693cd2e36e..039a9236fa 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -1,4 +1,3 @@ export { validate } from './validate.js'; export { SchemaValidationError } from './errors.js'; export { validator } from './decorator.js'; -export { validationMiddleware } from './middleware.js'; \ No newline at end of file diff --git a/packages/validation/src/middleware.ts b/packages/validation/src/middleware.ts index cb7cc8c88c..e706d6c846 100644 --- a/packages/validation/src/middleware.ts +++ b/packages/validation/src/middleware.ts @@ -2,10 +2,7 @@ import { SchemaValidationError } from './errors.js'; import type { ValidatorOptions } from './types.js'; import { validate } from './validate.js'; -export function validationMiddleware(options: ValidatorOptions) { - if (!options.inboundSchema && !options.outboundSchema) { - return {}; - } +export function validation(options: ValidatorOptions) { return { before: async (handler: { event: unknown }) => { if (options.inboundSchema) { diff --git a/packages/validation/tests/unit/middleware.test.ts b/packages/validation/tests/unit/middleware.test.ts index 415374e660..204c61aadb 100644 --- a/packages/validation/tests/unit/middleware.test.ts +++ b/packages/validation/tests/unit/middleware.test.ts @@ -1,6 +1,7 @@ +import middy from '@middy/core'; import { describe, expect, it } from 'vitest'; import { SchemaValidationError } from '../../src/errors.js'; -import { validationMiddleware } from '../../src/middleware.js'; +import { validation } from '../../src/middleware.js'; const inboundSchema = { type: 'object', @@ -20,68 +21,53 @@ const outboundSchema = { additionalProperties: false, }; -describe('validatorMiddleware', () => { +const response = { outputValue: 20 }; +const baseHandler = async (event: unknown) => { + return response; +}; + +describe('validation middleware with Middy', () => { it('should validate inbound and outbound successfully', async () => { // Prepare - const middleware = validationMiddleware({ inboundSchema, outboundSchema }); - const handler = { - event: { inputValue: 10 }, - response: { outputValue: 20 }, - }; + const middleware = validation({ inboundSchema, outboundSchema }); + const wrappedHandler = middy(baseHandler).use(middleware); + const event = { inputValue: 10 }; // Act - if (middleware.before) { - await middleware.before(handler); - } - if (middleware.after) { - await middleware.after(handler); - } + const result = await wrappedHandler(event); // Assess - expect(handler.event).toEqual({ inputValue: 10 }); - expect(handler.response).toEqual({ outputValue: 20 }); + expect(result).toEqual(response); }); it('should throw error on inbound validation failure', async () => { // Prepare - const middleware = validationMiddleware({ inboundSchema }); - const handler = { - event: { inputValue: 'invalid' }, - response: {}, - }; + const middleware = validation({ inboundSchema }); + const wrappedHandler = middy(baseHandler).use(middleware); + const invalidEvent = { inputValue: 'invalid' }; // Act & Assess - await expect(middleware.before?.(handler)).rejects.toThrow( + await expect(wrappedHandler(invalidEvent)).rejects.toThrow( SchemaValidationError ); }); it('should throw error on outbound validation failure', async () => { - // Prepare - const middleware = validationMiddleware({ outboundSchema }); - const handler = { - event: {}, - response: { outputValue: 'invalid' }, + const invalidHandler = async (_event: unknown) => { + return { outputValue: 'invalid' }; }; + const middleware = validation({ outboundSchema }); + const wrappedHandler = middy(invalidHandler).use(middleware); + const event = { any: 'value' }; // Act & Assess - await expect(middleware.after?.(handler)).rejects.toThrow( - SchemaValidationError - ); + await expect(wrappedHandler(event)).rejects.toThrow(SchemaValidationError); }); it('should no-op when no schemas are provided', async () => { // Prepare - const middleware = validationMiddleware({}); - const handler = { - event: { someKey: 'value' }, - response: { anotherKey: 'value' }, - }; + const middleware = validation({}); + const wrappedHandler = middy(baseHandler).use(middleware); + const event = { anyKey: 'anyValue' }; // Act - if (middleware.before) { - await middleware.before(handler); - } - if (middleware.after) { - await middleware.after(handler); - } + const result = await wrappedHandler(event); // Assess - expect(handler.event).toEqual({ someKey: 'value' }); - expect(handler.response).toEqual({ anotherKey: 'value' }); + expect(result).toEqual(response); }); });