From 1c1b1f19c0eb21261cf67dc8ab35b7f91369987f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 12:24:35 +0600 Subject: [PATCH 01/16] refactor: Update HandlerResponse type to use JSONValue instead of JSONObject --- packages/event-handler/src/types/rest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/types/rest.ts b/packages/event-handler/src/types/rest.ts index 4fa17c36a2..f2d002d210 100644 --- a/packages/event-handler/src/types/rest.ts +++ b/packages/event-handler/src/types/rest.ts @@ -1,7 +1,7 @@ import type { Readable } from 'node:stream'; import type { GenericLogger, - JSONObject, + JSONValue, } from '@aws-lambda-powertools/commons/types'; import type { APIGatewayProxyEvent, @@ -65,7 +65,7 @@ type ExtendedAPIGatewayProxyResult = Omit & { body: ExtendedAPIGatewayProxyResultBody; }; -type HandlerResponse = Response | JSONObject | ExtendedAPIGatewayProxyResult; +type HandlerResponse = Response | JSONValue | ExtendedAPIGatewayProxyResult; type RouteHandler = ( reqCtx: RequestContext From e1d5afcdb0907907bb07d4a01c9e1082dcc3f736 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 16:09:56 +0600 Subject: [PATCH 02/16] refactor: Enhance error handling by adding #errorBodyToResponse and #tryAddErrorCodeToBody methods --- packages/event-handler/src/rest/Router.ts | 60 +++++++++++++++++------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index f8ee13e7a9..f37f1c083f 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -1,7 +1,12 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import type streamWeb from 'node:stream/web'; -import type { GenericLogger } from '@aws-lambda-powertools/commons/types'; +import type { + GenericLogger, + JSONObject, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; +import { isRecord } from '@aws-lambda-powertools/commons/typeutils'; import { getStringFromEnv, isDevMode, @@ -386,19 +391,7 @@ class Router { if (body instanceof Response || isExtendedAPIGatewayProxyResult(body)) { return body; } - if (!body.statusCode) { - if (error instanceof NotFoundError) { - body.statusCode = HttpStatusCodes.NOT_FOUND; - } else if (error instanceof MethodNotAllowedError) { - body.statusCode = HttpStatusCodes.METHOD_NOT_ALLOWED; - } - } - return new Response(JSON.stringify(body), { - status: - (body.statusCode as number) ?? - HttpStatusCodes.INTERNAL_SERVER_ERROR, - headers: { 'Content-Type': 'application/json' }, - }); + return this.#errorBodyToResponse(body, error); } catch (handlerError) { if (handlerError instanceof HttpError) { return await this.handleError(handlerError, options); @@ -417,6 +410,45 @@ class Router { return this.#defaultErrorHandler(error); } + /** + * Converts an error handler's response body to an HTTP Response object. + * + * If the body is a record object without a status code, sets the status code for + * NotFoundError (404) or MethodNotAllowedError (405). Uses the status code from + * the body if present, otherwise defaults to 500 Internal Server Error. + * + * @param body - The response body returned by the error handler, of type JSONValue + * @param error - The Error object associated with the response + */ + #errorBodyToResponse(body: JSONValue, error: Error): Response { + let status: number = HttpStatusCodes.INTERNAL_SERVER_ERROR; + + if (isRecord(body)) { + if (!body.statusCode) this.#tryAddErrorCodeToBody(body, error); + status = (body.statusCode as number) ?? status; + } + + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + } + + /** + * Attempts to add the appropriate HTTP status code to the response body based on the error type. + * Only sets status codes for known error types (NotFoundError, MethodNotAllowedError). + * + * @param body - The response body object to which the status code will be added + * @param error - The error instance to check and map to an HTTP status code + */ + #tryAddErrorCodeToBody(body: JSONObject, error: Error): void { + if (error instanceof NotFoundError) { + body.statusCode = HttpStatusCodes.NOT_FOUND; + } else if (error instanceof MethodNotAllowedError) { + body.statusCode = HttpStatusCodes.METHOD_NOT_ALLOWED; + } + } + /** * Default error handler that returns a 500 Internal Server Error response. * In development mode, includes stack trace and error details. From 4517ec3456edda380189808a0c5882158c80cd55 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 16:15:58 +0600 Subject: [PATCH 03/16] refactor: Rename #tryAddErrorCodeToBody to #tryAddingErrorCodeToBody for clarity --- packages/event-handler/src/rest/Router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index f37f1c083f..39759f76cb 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -424,7 +424,7 @@ class Router { let status: number = HttpStatusCodes.INTERNAL_SERVER_ERROR; if (isRecord(body)) { - if (!body.statusCode) this.#tryAddErrorCodeToBody(body, error); + if (!body.statusCode) this.#tryAddingErrorCodeToBody(body, error); status = (body.statusCode as number) ?? status; } @@ -441,7 +441,7 @@ class Router { * @param body - The response body object to which the status code will be added * @param error - The error instance to check and map to an HTTP status code */ - #tryAddErrorCodeToBody(body: JSONObject, error: Error): void { + #tryAddingErrorCodeToBody(body: JSONObject, error: Error): void { if (error instanceof NotFoundError) { body.statusCode = HttpStatusCodes.NOT_FOUND; } else if (error instanceof MethodNotAllowedError) { From b50a14db461d0db4fc074e893ab61a1fd7d92d0c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 16:50:09 +0600 Subject: [PATCH 04/16] refactor: HTTP method tests for array response --- .../unit/rest/Router/basic-routing.test.ts | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts index 3fdd5b0954..ce9a587cce 100644 --- a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts @@ -10,7 +10,7 @@ import type { HttpMethod, RouteHandler } from '../../../../src/types/rest.js'; import { createTestEvent } from '../helpers.js'; describe('Class: Router - Basic Routing', () => { - it.each([ + const httpMethods = [ ['GET', 'get'], ['POST', 'post'], ['PUT', 'put'], @@ -18,25 +18,69 @@ describe('Class: Router - Basic Routing', () => { ['DELETE', 'delete'], ['HEAD', 'head'], ['OPTIONS', 'options'], - ])('routes %s requests', async (method, verb) => { - // Prepare - const app = new Router(); - ( - app[verb as Lowercase] as ( - path: string, - handler: RouteHandler - ) => void - )('/test', async () => ({ result: `${verb}-test` })); - // Act - const actual = await app.resolve(createTestEvent('/test', method), context); - // Assess - expect(actual).toEqual({ - statusCode: 200, - body: JSON.stringify({ result: `${verb}-test` }), - headers: { 'content-type': 'application/json' }, - isBase64Encoded: false, - }); - }); + ]; + + it.each(httpMethods)( + 'routes %s requests with object response', + async (method, verb) => { + // Prepare + const app = new Router(); + ( + app[verb as Lowercase] as ( + path: string, + handler: RouteHandler + ) => void + )('/test', async () => ({ result: `${verb}-test` })); + + // Act + const actual = await app.resolve( + createTestEvent('/test', method), + context + ); + + // Assess + expect(actual).toEqual({ + statusCode: 200, + body: JSON.stringify({ result: `${verb}-test` }), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + } + ); + + it.each(httpMethods)( + 'routes %s requests with array response', + async (method, verb) => { + // Prepare + const app = new Router(); + ( + app[verb as Lowercase] as ( + path: string, + handler: RouteHandler + ) => void + )('/test', async () => [ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]); + + // Act + const actual = await app.resolve( + createTestEvent('/test', method), + context + ); + + // Assess + expect(actual).toEqual({ + statusCode: 200, + body: JSON.stringify([ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + } + ); it.each([['CONNECT'], ['TRACE']])( 'throws MethodNotAllowedError for %s requests', From 5d2d7b343c771d010d1f79ca7635ba68f99f094e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 17:01:52 +0600 Subject: [PATCH 05/16] refactor: Update decorators tests to return expected responses for both object and array formats --- .../tests/unit/rest/Router/decorators.test.ts | 147 +++++++++++++----- 1 file changed, 105 insertions(+), 42 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index c08914b55d..4595ade0e5 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -30,58 +30,121 @@ const createStreamHandler = app.resolveStream(event, _context, { scope, responseStream }); describe('Class: Router - Decorators', () => { - describe('decorators', () => { - const app = new Router(); + describe.each([ + ['GET', 'get'], + ['POST', 'post'], + ['PUT', 'put'], + ['PATCH', 'patch'], + ['DELETE', 'delete'], + ['HEAD', 'head'], + ['OPTIONS', 'options'], + ])('routes %s requests', (method, verb) => { + it('with object response', async () => { + // Prepare + const app = new Router(); + const expectedResponse = { result: `${verb}-test` }; - class Lambda { - @app.get('/test') - public getTest() { - return { result: 'get-test' }; - } + class Lambda { + @app.get('/test') + public getTest() { + return expectedResponse; + } - @app.post('/test') - public postTest() { - return { result: 'post-test' }; - } + @app.post('/test') + public postTest() { + return expectedResponse; + } - @app.put('/test') - public putTest() { - return { result: 'put-test' }; - } + @app.put('/test') + public putTest() { + return expectedResponse; + } - @app.patch('/test') - public patchTest() { - return { result: 'patch-test' }; - } + @app.patch('/test') + public patchTest() { + return expectedResponse; + } - @app.delete('/test') - public deleteTest() { - return { result: 'delete-test' }; - } + @app.delete('/test') + public deleteTest() { + return expectedResponse; + } - @app.head('/test') - public headTest() { - return { result: 'head-test' }; - } + @app.head('/test') + public headTest() { + return expectedResponse; + } - @app.options('/test') - public optionsTest() { - return { result: 'options-test' }; + @app.options('/test') + public optionsTest() { + return expectedResponse; + } + + public handler = createHandler(app); } - public handler = createHandler(app); - } + const lambda = new Lambda(); + // Act + const actual = await lambda.handler( + createTestEvent('/test', method), + context + ); + // Assess + expect(actual).toEqual({ + statusCode: 200, + body: JSON.stringify(expectedResponse), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); - it.each([ - ['GET', { result: 'get-test' }], - ['POST', { result: 'post-test' }], - ['PUT', { result: 'put-test' }], - ['PATCH', { result: 'patch-test' }], - ['DELETE', { result: 'delete-test' }], - ['HEAD', { result: 'head-test' }], - ['OPTIONS', { result: 'options-test' }], - ])('routes %s requests with decorators', async (method, expected) => { + it('with array response', async () => { // Prepare + const app = new Router(); + const expectedResponse = [ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]; + + class Lambda { + @app.get('/test') + public getTest() { + return expectedResponse; + } + + @app.post('/test') + public postTest() { + return expectedResponse; + } + + @app.put('/test') + public putTest() { + return expectedResponse; + } + + @app.patch('/test') + public patchTest() { + return expectedResponse; + } + + @app.delete('/test') + public deleteTest() { + return expectedResponse; + } + + @app.head('/test') + public headTest() { + return expectedResponse; + } + + @app.options('/test') + public optionsTest() { + return expectedResponse; + } + + public handler = createHandler(app); + } + const lambda = new Lambda(); // Act const actual = await lambda.handler( @@ -91,7 +154,7 @@ describe('Class: Router - Decorators', () => { // Assess expect(actual).toEqual({ statusCode: 200, - body: JSON.stringify(expected), + body: JSON.stringify(expectedResponse), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, }); From 829e20afadc13238875403007f1125843c99632f Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 17:03:25 +0600 Subject: [PATCH 06/16] refactor: Simplify HTTP method tests by using describe.each for request routing --- .../unit/rest/Router/basic-routing.test.ts | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts index ce9a587cce..ffdbeaadf5 100644 --- a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts @@ -10,7 +10,7 @@ import type { HttpMethod, RouteHandler } from '../../../../src/types/rest.js'; import { createTestEvent } from '../helpers.js'; describe('Class: Router - Basic Routing', () => { - const httpMethods = [ + describe.each([ ['GET', 'get'], ['POST', 'post'], ['PUT', 'put'], @@ -18,11 +18,8 @@ describe('Class: Router - Basic Routing', () => { ['DELETE', 'delete'], ['HEAD', 'head'], ['OPTIONS', 'options'], - ]; - - it.each(httpMethods)( - 'routes %s requests with object response', - async (method, verb) => { + ])('routes %s requests', (method, verb) => { + it('with object response', async () => { // Prepare const app = new Router(); ( @@ -45,12 +42,9 @@ describe('Class: Router - Basic Routing', () => { headers: { 'content-type': 'application/json' }, isBase64Encoded: false, }); - } - ); + }); - it.each(httpMethods)( - 'routes %s requests with array response', - async (method, verb) => { + it('with array response', async () => { // Prepare const app = new Router(); ( @@ -79,8 +73,8 @@ describe('Class: Router - Basic Routing', () => { headers: { 'content-type': 'application/json' }, isBase64Encoded: false, }); - } - ); + }); + }); it.each([['CONNECT'], ['TRACE']])( 'throws MethodNotAllowedError for %s requests', From ef33697271e0f2697089ebd5ec092dcf35a025fd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 9 Nov 2025 17:03:39 +0600 Subject: [PATCH 07/16] test: Add test for handling array response from error handler --- .../unit/rest/Router/error-handling.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts index 3c7aaea176..8fc3f22230 100644 --- a/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/error-handling.test.ts @@ -482,6 +482,28 @@ describe('Class: Router - Error Handling', () => { }); }); + it('handles returning an array from the error handler', async () => { + // Prepare + const app = new Router(); + + app.errorHandler(BadRequestError, async () => ['error1', 'error2']); + + app.get('/test', () => { + throw new BadRequestError('test error'); + }); + + // Act + const result = await app.resolve(createTestEvent('/test', 'GET'), context); + + // Assess + expect(result).toEqual({ + statusCode: HttpStatusCodes.INTERNAL_SERVER_ERROR, + body: JSON.stringify(['error1', 'error2']), + headers: { 'content-type': 'application/json' }, + isBase64Encoded: false, + }); + }); + it('handles throwing a built in NotFound error from the error handler', async () => { // Prepare const app = new Router(); From 6c2ad1f3eff35a9657190442193f08009c87b3d7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 10 Nov 2025 11:17:05 +0600 Subject: [PATCH 08/16] refactor: Simplify error handling by extracting status code logic from body --- packages/event-handler/src/rest/Router.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 39759f76cb..162714d516 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -3,7 +3,6 @@ import { pipeline } from 'node:stream/promises'; import type streamWeb from 'node:stream/web'; import type { GenericLogger, - JSONObject, JSONValue, } from '@aws-lambda-powertools/commons/types'; import { isRecord } from '@aws-lambda-powertools/commons/typeutils'; @@ -424,7 +423,7 @@ class Router { let status: number = HttpStatusCodes.INTERNAL_SERVER_ERROR; if (isRecord(body)) { - if (!body.statusCode) this.#tryAddingErrorCodeToBody(body, error); + body.statusCode = body.statusCode ?? this.#getStatusCodeFromError(error); status = (body.statusCode as number) ?? status; } @@ -435,17 +434,20 @@ class Router { } /** - * Attempts to add the appropriate HTTP status code to the response body based on the error type. - * Only sets status codes for known error types (NotFoundError, MethodNotAllowedError). + * Extracts the HTTP status code from an error instance. * - * @param body - The response body object to which the status code will be added - * @param error - The error instance to check and map to an HTTP status code + * Maps specific error types to their corresponding HTTP status codes: + * - `NotFoundError` maps to 404 (NOT_FOUND) + * - `MethodNotAllowedError` maps to 405 (METHOD_NOT_ALLOWED) + * + * @param error - The error instance to extract the status code from */ - #tryAddingErrorCodeToBody(body: JSONObject, error: Error): void { + #getStatusCodeFromError(error: Error): number | undefined { if (error instanceof NotFoundError) { - body.statusCode = HttpStatusCodes.NOT_FOUND; - } else if (error instanceof MethodNotAllowedError) { - body.statusCode = HttpStatusCodes.METHOD_NOT_ALLOWED; + return HttpStatusCodes.NOT_FOUND; + } + if (error instanceof MethodNotAllowedError) { + return HttpStatusCodes.METHOD_NOT_ALLOWED; } } From cbd789d783204aff795eb95ab447d42eed14507b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 10 Nov 2025 11:22:51 +0600 Subject: [PATCH 09/16] refactor: Update response body in decorators tests to reflect expected structure --- .../tests/unit/rest/Router/decorators.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index 4595ade0e5..9d44046d85 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -92,7 +92,7 @@ describe('Class: Router - Decorators', () => { // Assess expect(actual).toEqual({ statusCode: 200, - body: JSON.stringify(expectedResponse), + body: JSON.stringify({ result: `${verb}-test` }), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, }); @@ -154,7 +154,10 @@ describe('Class: Router - Decorators', () => { // Assess expect(actual).toEqual({ statusCode: 200, - body: JSON.stringify(expectedResponse), + body: JSON.stringify([ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]), headers: { 'content-type': 'application/json' }, isBase64Encoded: false, }); From 4605774ece21eced4cca4cca030f1a218ca8856e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 10 Nov 2025 19:44:45 +0600 Subject: [PATCH 10/16] test: Enhance routing tests to include array response handling --- .../unit/rest/Router/basic-routing.test.ts | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts index 555c33c6e8..da37e7b9e7 100644 --- a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts @@ -13,7 +13,7 @@ describe.each([ { version: 'V1', createEvent: createTestEvent }, { version: 'V2', createEvent: createTestEventV2 }, ])('Class: Router - Basic Routing ($version)', ({ createEvent }) => { - it.each([ + describe.each([ ['GET', 'get'], ['POST', 'post'], ['PUT', 'put'], @@ -21,22 +21,54 @@ describe.each([ ['DELETE', 'delete'], ['HEAD', 'head'], ['OPTIONS', 'options'], - ])('routes %s requests', async (method, verb) => { - // Prepare - const app = new Router(); - ( - app[verb as Lowercase] as ( - path: string, - handler: RouteHandler - ) => void - )('/test', async () => ({ result: `${verb}-test` })); - // Act - const actual = await app.resolve(createEvent('/test', method), context); - // Assess - expect(actual.statusCode).toBe(200); - expect(actual.body).toBe(JSON.stringify({ result: `${verb}-test` })); - expect(actual.headers?.['content-type']).toBe('application/json'); - expect(actual.isBase64Encoded).toBe(false); + ])('routes %s requests', (method, verb) => { + it('with object response', async () => { + // Prepare + const app = new Router(); + ( + app[verb as Lowercase] as ( + path: string, + handler: RouteHandler + ) => void + )('/test', async () => ({ result: `${verb}-test` })); + + // Act + const actual = await app.resolve(createEvent('/test', method), context); + + // Assess + expect(actual.statusCode).toBe(200); + expect(actual.body).toBe(JSON.stringify({ result: `${verb}-test` })); + expect(actual.headers?.['content-type']).toBe('application/json'); + expect(actual.isBase64Encoded).toBe(false); + }); + + it('with array response', async () => { + // Prepare + const app = new Router(); + ( + app[verb as Lowercase] as ( + path: string, + handler: RouteHandler + ) => void + )('/test', async () => [ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]); + + // Act + const actual = await app.resolve(createEvent('/test', method), context); + + // Assess + expect(actual.statusCode).toBe(200); + expect(actual.body).toBe( + JSON.stringify([ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]) + ); + expect(actual.headers?.['content-type']).toBe('application/json'); + expect(actual.isBase64Encoded).toBe(false); + }); }); it.each([['CONNECT'], ['TRACE']])( From 1f262afcb72fe2bb5e748304e229d91ec6c1362d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Mon, 10 Nov 2025 19:51:17 +0600 Subject: [PATCH 11/16] refactor: Consolidate HTTP method tests for object and array responses --- .../unit/rest/Router/basic-routing.test.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts index da37e7b9e7..d9ed909c7a 100644 --- a/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts @@ -13,7 +13,7 @@ describe.each([ { version: 'V1', createEvent: createTestEvent }, { version: 'V2', createEvent: createTestEventV2 }, ])('Class: Router - Basic Routing ($version)', ({ createEvent }) => { - describe.each([ + const httpMethods = [ ['GET', 'get'], ['POST', 'post'], ['PUT', 'put'], @@ -21,8 +21,10 @@ describe.each([ ['DELETE', 'delete'], ['HEAD', 'head'], ['OPTIONS', 'options'], - ])('routes %s requests', (method, verb) => { - it('with object response', async () => { + ]; + it.each(httpMethods)( + 'routes %s requests with object response', + async (method, verb) => { // Prepare const app = new Router(); ( @@ -40,9 +42,12 @@ describe.each([ expect(actual.body).toBe(JSON.stringify({ result: `${verb}-test` })); expect(actual.headers?.['content-type']).toBe('application/json'); expect(actual.isBase64Encoded).toBe(false); - }); + } + ); - it('with array response', async () => { + it.each(httpMethods)( + 'routes %s requests with array response', + async (method, verb) => { // Prepare const app = new Router(); ( @@ -68,8 +73,8 @@ describe.each([ ); expect(actual.headers?.['content-type']).toBe('application/json'); expect(actual.isBase64Encoded).toBe(false); - }); - }); + } + ); it.each([['CONNECT'], ['TRACE']])( 'throws MethodNotAllowedError for %s requests', From d2d98f666dfd1b146defc514af6cee86b69545f4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 11 Nov 2025 10:29:04 +0600 Subject: [PATCH 12/16] test: Add array response handling for all HTTP methods in decorators tests --- .../tests/unit/rest/Router/decorators.test.ts | 153 +++++++++++++----- 1 file changed, 110 insertions(+), 43 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index 44715314e3..4af635524d 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -72,69 +72,136 @@ describe.each([ { version: 'V2', createEvent: createTestEventV2 }, ])('Class: Router - Decorators ($version)', ({ createEvent }) => { describe('decorators', () => { - const app = new Router(); + const httpMethods = [ + ['GET', 'get'], + ['POST', 'post'], + ['PUT', 'put'], + ['PATCH', 'patch'], + ['DELETE', 'delete'], + ['HEAD', 'head'], + ['OPTIONS', 'options'], + ]; + it.each(httpMethods)('routes %s requests', async (method, verb) => { + // Prepare + const app = new Router(); + const expected = { result: `${verb}-test` }; - class Lambda { - @app.get('/test') - public getTest() { - return { result: 'get-test' }; - } + class Lambda { + @app.get('/test') + public getTest() { + return expected; + } - @app.post('/test') - public postTest() { - return { result: 'post-test' }; - } + @app.post('/test') + public postTest() { + return expected; + } - @app.put('/test') - public putTest() { - return { result: 'put-test' }; - } + @app.put('/test') + public putTest() { + return expected; + } - @app.patch('/test') - public patchTest() { - return { result: 'patch-test' }; - } + @app.patch('/test') + public patchTest() { + return expected; + } - @app.delete('/test') - public deleteTest() { - return { result: 'delete-test' }; - } + @app.delete('/test') + public deleteTest() { + return expected; + } - @app.head('/test') - public headTest() { - return { result: 'head-test' }; - } + @app.head('/test') + public headTest() { + return expected; + } - @app.options('/test') - public optionsTest() { - return { result: 'options-test' }; - } + @app.options('/test') + public optionsTest() { + return expected; + } - public handler = createHandler(app); - } + public handler = createHandler(app); + } - it.each([ - ['GET', { result: 'get-test' }], - ['POST', { result: 'post-test' }], - ['PUT', { result: 'put-test' }], - ['PATCH', { result: 'patch-test' }], - ['DELETE', { result: 'delete-test' }], - ['HEAD', { result: 'head-test' }], - ['OPTIONS', { result: 'options-test' }], - ])('routes %s requests with decorators', async (method, expected) => { - // Prepare const lambda = new Lambda(); + // Act const actual = await lambda.handler( - createEvent('/test', method), + createTestEvent('/test', method), context ); + // Assess expect(actual.statusCode).toBe(200); expect(actual.body).toBe(JSON.stringify(expected)); expect(actual.headers?.['content-type']).toBe('application/json'); expect(actual.isBase64Encoded).toBe(false); }); + + it.each(httpMethods)( + 'routes %s requests with array response', + async (method, verb) => { + // Prepare + const app = new Router(); + const expected = [ + { id: 1, result: `${verb}-test-1` }, + { id: 2, result: `${verb}-test-2` }, + ]; + + class Lambda { + @app.get('/test') + public getTest() { + return expected; + } + + @app.post('/test') + public postTest() { + return expected; + } + + @app.put('/test') + public putTest() { + return expected; + } + + @app.patch('/test') + public patchTest() { + return expected; + } + + @app.delete('/test') + public deleteTest() { + return expected; + } + + @app.head('/test') + public headTest() { + return expected; + } + + @app.options('/test') + public optionsTest() { + return expected; + } + + public handler = createHandler(app); + } + + const lambda = new Lambda(); + // Act + const actual = await lambda.handler( + createTestEvent('/test', method), + context + ); + // Assess + expect(actual.statusCode).toBe(200); + expect(actual.body).toBe(JSON.stringify(expected)); + expect(actual.headers?.['content-type']).toBe('application/json'); + expect(actual.isBase64Encoded).toBe(false); + } + ); }); describe('decorators with middleware', () => { From 849dab161b7b511238e44266a92a1e8341dde002 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 11 Nov 2025 10:32:44 +0600 Subject: [PATCH 13/16] test: Update request routing test description to specify object response --- .../tests/unit/rest/Router/decorators.test.ts | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index 4af635524d..ca5d62a14e 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -81,64 +81,67 @@ describe.each([ ['HEAD', 'head'], ['OPTIONS', 'options'], ]; - it.each(httpMethods)('routes %s requests', async (method, verb) => { - // Prepare - const app = new Router(); - const expected = { result: `${verb}-test` }; + it.each(httpMethods)( + 'routes %s requests with object response', + async (method, verb) => { + // Prepare + const app = new Router(); + const expected = { result: `${verb}-test` }; - class Lambda { - @app.get('/test') - public getTest() { - return expected; - } + class Lambda { + @app.get('/test') + public getTest() { + return expected; + } - @app.post('/test') - public postTest() { - return expected; - } + @app.post('/test') + public postTest() { + return expected; + } - @app.put('/test') - public putTest() { - return expected; - } + @app.put('/test') + public putTest() { + return expected; + } - @app.patch('/test') - public patchTest() { - return expected; - } + @app.patch('/test') + public patchTest() { + return expected; + } - @app.delete('/test') - public deleteTest() { - return expected; - } + @app.delete('/test') + public deleteTest() { + return expected; + } - @app.head('/test') - public headTest() { - return expected; - } + @app.head('/test') + public headTest() { + return expected; + } - @app.options('/test') - public optionsTest() { - return expected; - } + @app.options('/test') + public optionsTest() { + return expected; + } - public handler = createHandler(app); - } + public handler = createHandler(app); + } - const lambda = new Lambda(); + const lambda = new Lambda(); - // Act - const actual = await lambda.handler( - createTestEvent('/test', method), - context - ); + // Act + const actual = await lambda.handler( + createTestEvent('/test', method), + context + ); - // Assess - expect(actual.statusCode).toBe(200); - expect(actual.body).toBe(JSON.stringify(expected)); - expect(actual.headers?.['content-type']).toBe('application/json'); - expect(actual.isBase64Encoded).toBe(false); - }); + // Assess + expect(actual.statusCode).toBe(200); + expect(actual.body).toBe(JSON.stringify(expected)); + expect(actual.headers?.['content-type']).toBe('application/json'); + expect(actual.isBase64Encoded).toBe(false); + } + ); it.each(httpMethods)( 'routes %s requests with array response', From f94addd20793aa8c9c70378c5eb97a50cbbf98d7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 11 Nov 2025 10:42:54 +0600 Subject: [PATCH 14/16] refactor: Extract Lambda class creation into a separate function for cleaner test setup --- .../tests/unit/rest/Router/decorators.test.ts | 127 +++++++----------- 1 file changed, 47 insertions(+), 80 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index ca5d62a14e..3a7c3fbdf2 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -67,6 +67,49 @@ const createStreamHandler = (event: unknown, _context: Context, responseStream: MockResponseStream) => app.resolveStream(event, _context, { scope, responseStream }); +const createTestLambdaClass = (app: Router, expectedResponse: unknown) => { + class Lambda { + @app.get('/test') + public getTest() { + return expectedResponse; + } + + @app.post('/test') + public postTest() { + return expectedResponse; + } + + @app.put('/test') + public putTest() { + return expectedResponse; + } + + @app.patch('/test') + public patchTest() { + return expectedResponse; + } + + @app.delete('/test') + public deleteTest() { + return expectedResponse; + } + + @app.head('/test') + public headTest() { + return expectedResponse; + } + + @app.options('/test') + public optionsTest() { + return expectedResponse; + } + + public handler = createHandler(app); + } + + return Lambda; +}; + describe.each([ { version: 'V1', createEvent: createTestEvent }, { version: 'V2', createEvent: createTestEventV2 }, @@ -87,46 +130,7 @@ describe.each([ // Prepare const app = new Router(); const expected = { result: `${verb}-test` }; - - class Lambda { - @app.get('/test') - public getTest() { - return expected; - } - - @app.post('/test') - public postTest() { - return expected; - } - - @app.put('/test') - public putTest() { - return expected; - } - - @app.patch('/test') - public patchTest() { - return expected; - } - - @app.delete('/test') - public deleteTest() { - return expected; - } - - @app.head('/test') - public headTest() { - return expected; - } - - @app.options('/test') - public optionsTest() { - return expected; - } - - public handler = createHandler(app); - } - + const Lambda = createTestLambdaClass(app, expected); const lambda = new Lambda(); // Act @@ -152,52 +156,15 @@ describe.each([ { id: 1, result: `${verb}-test-1` }, { id: 2, result: `${verb}-test-2` }, ]; - - class Lambda { - @app.get('/test') - public getTest() { - return expected; - } - - @app.post('/test') - public postTest() { - return expected; - } - - @app.put('/test') - public putTest() { - return expected; - } - - @app.patch('/test') - public patchTest() { - return expected; - } - - @app.delete('/test') - public deleteTest() { - return expected; - } - - @app.head('/test') - public headTest() { - return expected; - } - - @app.options('/test') - public optionsTest() { - return expected; - } - - public handler = createHandler(app); - } - + const Lambda = createTestLambdaClass(app, expected); const lambda = new Lambda(); + // Act const actual = await lambda.handler( createTestEvent('/test', method), context ); + // Assess expect(actual.statusCode).toBe(200); expect(actual.body).toBe(JSON.stringify(expected)); From d20ac1ff7013fdb4b4dfd96fd3745d4c76378f2a Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 11 Nov 2025 17:44:19 +0600 Subject: [PATCH 15/16] refactor: Rename errorBodyToResponse method to errorBodyToWebResponse for clarity --- packages/event-handler/src/rest/Router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/src/rest/Router.ts b/packages/event-handler/src/rest/Router.ts index 6d32945d30..512ed2189b 100644 --- a/packages/event-handler/src/rest/Router.ts +++ b/packages/event-handler/src/rest/Router.ts @@ -461,7 +461,7 @@ class Router { ) { return body; } - return this.#errorBodyToResponse(body, error); + return this.#errorBodyToWebResponse(body, error); } catch (handlerError) { if (handlerError instanceof HttpError) { return await this.handleError(handlerError, options); @@ -490,7 +490,7 @@ class Router { * @param body - The response body returned by the error handler, of type JSONValue * @param error - The Error object associated with the response */ - #errorBodyToResponse(body: JSONValue, error: Error): Response { + #errorBodyToWebResponse(body: JSONValue, error: Error): Response { let status: number = HttpStatusCodes.INTERNAL_SERVER_ERROR; if (isRecord(body)) { From bdfc0536bdae966731491b38e188ad14a2b3a77b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 11 Nov 2025 18:24:23 +0600 Subject: [PATCH 16/16] refactor: Move handler creation functions to helpers for better organization --- .../tests/unit/rest/Router/decorators.test.ts | 97 +---------------- .../event-handler/tests/unit/rest/helpers.ts | 102 +++++++++++++++++- 2 files changed, 105 insertions(+), 94 deletions(-) diff --git a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts index 3a7c3fbdf2..f48fd194f4 100644 --- a/packages/event-handler/tests/unit/rest/Router/decorators.test.ts +++ b/packages/event-handler/tests/unit/rest/Router/decorators.test.ts @@ -1,11 +1,4 @@ import context from '@aws-lambda-powertools/testing-utils/context'; -import type { - APIGatewayProxyEvent, - APIGatewayProxyEventV2, - APIGatewayProxyResult, - APIGatewayProxyStructuredResultV2, - Context, -} from 'aws-lambda'; import { describe, expect, it } from 'vitest'; import { BadRequestError, @@ -17,99 +10,17 @@ import { } from '../../../../src/rest/index.js'; import type { RequestContext } from '../../../../src/types/rest.js'; import { + createHandler, + createHandlerWithScope, + createStreamHandler, createTestEvent, createTestEventV2, + createTestLambdaClass, createTrackingMiddleware, MockResponseStream, parseStreamOutput, } from '../helpers.js'; -const createHandler = (app: Router) => { - function handler( - event: APIGatewayProxyEvent, - context: Context - ): Promise; - function handler( - event: APIGatewayProxyEventV2, - context: Context - ): Promise; - function handler( - event: unknown, - context: Context - ): Promise; - function handler(event: unknown, context: Context) { - return app.resolve(event, context); - } - return handler; -}; - -const createHandlerWithScope = (app: Router, scope: unknown) => { - function handler( - event: APIGatewayProxyEvent, - context: Context - ): Promise; - function handler( - event: APIGatewayProxyEventV2, - context: Context - ): Promise; - function handler( - event: unknown, - context: Context - ): Promise; - function handler(event: unknown, context: Context) { - return app.resolve(event, context, { scope }); - } - return handler; -}; - -const createStreamHandler = - (app: Router, scope: unknown) => - (event: unknown, _context: Context, responseStream: MockResponseStream) => - app.resolveStream(event, _context, { scope, responseStream }); - -const createTestLambdaClass = (app: Router, expectedResponse: unknown) => { - class Lambda { - @app.get('/test') - public getTest() { - return expectedResponse; - } - - @app.post('/test') - public postTest() { - return expectedResponse; - } - - @app.put('/test') - public putTest() { - return expectedResponse; - } - - @app.patch('/test') - public patchTest() { - return expectedResponse; - } - - @app.delete('/test') - public deleteTest() { - return expectedResponse; - } - - @app.head('/test') - public headTest() { - return expectedResponse; - } - - @app.options('/test') - public optionsTest() { - return expectedResponse; - } - - public handler = createHandler(app); - } - - return Lambda; -}; - describe.each([ { version: 'V1', createEvent: createTestEvent }, { version: 'V2', createEvent: createTestEventV2 }, diff --git a/packages/event-handler/tests/unit/rest/helpers.ts b/packages/event-handler/tests/unit/rest/helpers.ts index 470e12ab74..9f5e9e6fcc 100644 --- a/packages/event-handler/tests/unit/rest/helpers.ts +++ b/packages/event-handler/tests/unit/rest/helpers.ts @@ -1,4 +1,11 @@ -import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda'; +import type { + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + APIGatewayProxyResult, + APIGatewayProxyStructuredResultV2, + Context, +} from 'aws-lambda'; +import type { Router } from '../../../src/rest/Router.js'; import { HttpResponseStream } from '../../../src/rest/utils.js'; import type { HandlerResponse, Middleware } from '../../../src/types/rest.js'; @@ -160,3 +167,96 @@ export function parseStreamOutput(chunks: Buffer[]) { body: bodyBuffer.toString(), }; } + +// Create a handler function from the Router instance +export const createHandler = (app: Router) => { + function handler( + event: APIGatewayProxyEvent, + context: Context + ): Promise; + function handler( + event: APIGatewayProxyEventV2, + context: Context + ): Promise; + function handler( + event: unknown, + context: Context + ): Promise; + function handler(event: unknown, context: Context) { + return app.resolve(event, context); + } + return handler; +}; + +// Create a handler function from the Router instance with a custom scope +export const createHandlerWithScope = (app: Router, scope: unknown) => { + function handler( + event: APIGatewayProxyEvent, + context: Context + ): Promise; + function handler( + event: APIGatewayProxyEventV2, + context: Context + ): Promise; + function handler( + event: unknown, + context: Context + ): Promise; + function handler(event: unknown, context: Context) { + return app.resolve(event, context, { scope }); + } + return handler; +}; + +// Create a stream handler function from the Router instance with a custom scope +export const createStreamHandler = + (app: Router, scope: unknown) => + (event: unknown, _context: Context, responseStream: MockResponseStream) => + app.resolveStream(event, _context, { scope, responseStream }); + +// Create a test Lambda class with all HTTP method decorators +export const createTestLambdaClass = ( + app: Router, + expectedResponse: unknown +) => { + class Lambda { + @app.get('/test') + public getTest() { + return expectedResponse; + } + + @app.post('/test') + public postTest() { + return expectedResponse; + } + + @app.put('/test') + public putTest() { + return expectedResponse; + } + + @app.patch('/test') + public patchTest() { + return expectedResponse; + } + + @app.delete('/test') + public deleteTest() { + return expectedResponse; + } + + @app.head('/test') + public headTest() { + return expectedResponse; + } + + @app.options('/test') + public optionsTest() { + return expectedResponse; + } + + public handler = createHandler(app); + } + + return Lambda; +};