From 7d920b7e77ac120d3cab5c1808945fdb5068887b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 23 Jan 2025 20:28:16 +0100 Subject: [PATCH] chore(parser): deprecate AlbMultiValueHeadersSchema --- packages/parser/src/schemas/alb.ts | 51 ++++-- .../events/{albEvent.json => alb/base.json} | 2 +- .../parser/tests/events/alb/multi-fields.json | 35 ++++ .../events/albEventPathTrailingSlash.json | 28 ---- .../events/albMultiValueHeadersEvent.json | 23 --- packages/parser/tests/unit/helpers.test.ts | 155 ++++++++---------- packages/parser/tests/unit/schema/alb.test.ts | 69 ++++++-- 7 files changed, 192 insertions(+), 171 deletions(-) rename packages/parser/tests/events/{albEvent.json => alb/base.json} (99%) create mode 100644 packages/parser/tests/events/alb/multi-fields.json delete mode 100644 packages/parser/tests/events/albEventPathTrailingSlash.json delete mode 100644 packages/parser/tests/events/albMultiValueHeadersEvent.json diff --git a/packages/parser/src/schemas/alb.ts b/packages/parser/src/schemas/alb.ts index 4afa283de2..f22e53fcbb 100644 --- a/packages/parser/src/schemas/alb.ts +++ b/packages/parser/src/schemas/alb.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; /** - * Zod schema for Application load balancer event + * Zod schema for Application Load Balancer events. * * @example * ```json @@ -33,6 +33,32 @@ import { z } from 'zod'; * } * ``` * + * With multi-value headers and multi-value query string parameters: + * + * @example + * ```json + * { + * "requestContext": { + * "elb": { + * "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09" + * } + * }, + * "httpMethod": "GET", + * "path": "/", + * "multiValueHeaders": { + * "Set-cookie": [ + * "cookie-name=cookie-value;Domain=myweb.com;Secure;HttpOnly", + * "cookie-name=cookie-value;Expires=May 8, 2019" + * ], + * "Content-Type": [ + * "application/json" + * ] + * }, + * "isBase64Encoded": false, + * "body": "request_body" + * } + * ``` + * * @see {@link types.ALBEvent | ALBEvent} * @see {@link https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html} * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html} @@ -43,7 +69,11 @@ const AlbSchema = z.object({ body: z.string(), isBase64Encoded: z.boolean(), headers: z.record(z.string(), z.string()).optional(), + multiValueHeaders: z.record(z.string(), z.array(z.string())).optional(), queryStringParameters: z.record(z.string(), z.string()).optional(), + multiValueQueryStringParameters: z + .record(z.string(), z.array(z.string())) + .optional(), requestContext: z.object({ elb: z.object({ targetGroupArn: z.string(), @@ -52,26 +82,15 @@ const AlbSchema = z.object({ }); /** - * Zod schema for Application load balancer event with multi-value headers + * @deprecated Use `AlbSchema` instead, which handles both types of headers & querystring parameters. * - * @example - * ```json - * { - * "multiValueHeaders": { - * "Set-cookie": [ - * "cookie-name=cookie-value;Domain=myweb.com;Secure;HttpOnly", - * "cookie-name=cookie-value;Expires=May 8, 2019" - * ], - * "Content-Type": [ - * "application/json" - * ] - * } - * } - * ``` + * This schema will be removed in a future major release. */ +/* v8 ignore start */ const AlbMultiValueHeadersSchema = AlbSchema.extend({ multiValueHeaders: z.record(z.string(), z.array(z.string())), multiValueQueryStringParameters: z.record(z.string(), z.array(z.string())), }); +/* v8 ignore stop */ export { AlbSchema, AlbMultiValueHeadersSchema }; diff --git a/packages/parser/tests/events/albEvent.json b/packages/parser/tests/events/alb/base.json similarity index 99% rename from packages/parser/tests/events/albEvent.json rename to packages/parser/tests/events/alb/base.json index 9328cb39e1..0ce1b680f0 100644 --- a/packages/parser/tests/events/albEvent.json +++ b/packages/parser/tests/events/alb/base.json @@ -25,4 +25,4 @@ }, "body": "Test", "isBase64Encoded": false -} +} \ No newline at end of file diff --git a/packages/parser/tests/events/alb/multi-fields.json b/packages/parser/tests/events/alb/multi-fields.json new file mode 100644 index 0000000000..e555b7840b --- /dev/null +++ b/packages/parser/tests/events/alb/multi-fields.json @@ -0,0 +1,35 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" + } + }, + "httpMethod": "GET", + "path": "/lambda", + "multiValueQueryStringParameters": {}, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "host": [ + "alb-c-LoadB-14POFKYCLBNSF-1815800096.eu-central-1.elb.amazonaws.com" + ], + "user-agent": [ + "curl/7.79.1" + ], + "x-amzn-trace-id": [ + "Root=1-62fa9327-21cdd4da4c6db451490a5fb7" + ], + "x-forwarded-for": [ + "123.123.123.123" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ] + }, + "body": "Test", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/packages/parser/tests/events/albEventPathTrailingSlash.json b/packages/parser/tests/events/albEventPathTrailingSlash.json deleted file mode 100644 index d365fa84b2..0000000000 --- a/packages/parser/tests/events/albEventPathTrailingSlash.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "requestContext": { - "elb": { - "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a" - } - }, - "httpMethod": "GET", - "path": "/lambda/", - "queryStringParameters": { - "query": "1234ABCD" - }, - "headers": { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "accept-encoding": "gzip", - "accept-language": "en-US,en;q=0.9", - "connection": "keep-alive", - "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com", - "upgrade-insecure-requests": "1", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", - "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", - "x-forwarded-for": "72.12.164.125", - "x-forwarded-port": "80", - "x-forwarded-proto": "http", - "x-imforwards": "20" - }, - "body": "Test", - "isBase64Encoded": false -} diff --git a/packages/parser/tests/events/albMultiValueHeadersEvent.json b/packages/parser/tests/events/albMultiValueHeadersEvent.json deleted file mode 100644 index 67421f5130..0000000000 --- a/packages/parser/tests/events/albMultiValueHeadersEvent.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "requestContext": { - "elb": { - "targetGroupArn": "arn:aws:elasticloadbalancing:eu-central-1:1234567890:targetgroup/alb-c-Targe-11GDXTPQ7663S/804a67588bfdc10f" - } - }, - "httpMethod": "GET", - "path": "/todos", - "multiValueQueryStringParameters": {}, - "multiValueHeaders": { - "accept": ["*/*"], - "host": [ - "alb-c-LoadB-14POFKYCLBNSF-1815800096.eu-central-1.elb.amazonaws.com" - ], - "user-agent": ["curl/7.79.1"], - "x-amzn-trace-id": ["Root=1-62fa9327-21cdd4da4c6db451490a5fb7"], - "x-forwarded-for": ["123.123.123.123"], - "x-forwarded-port": ["80"], - "x-forwarded-proto": ["http"] - }, - "body": "", - "isBase64Encoded": false -} diff --git a/packages/parser/tests/unit/helpers.test.ts b/packages/parser/tests/unit/helpers.test.ts index 4df9d32be1..b8c49ac2d6 100644 --- a/packages/parser/tests/unit/helpers.test.ts +++ b/packages/parser/tests/unit/helpers.test.ts @@ -6,7 +6,7 @@ import { AlbSchema } from '../../src/schemas/alb.js'; import { DynamoDBStreamRecord, DynamoDBStreamSchema, -} from '../../src/schemas/dynamodb'; +} from '../../src/schemas/dynamodb.js'; import { SnsNotificationSchema, SnsRecordSchema, @@ -33,8 +33,8 @@ const basePayload = { email: 'foo@bar.baz', }; -describe('JSONStringified', () => { - it('should return a valid JSON', () => { +describe('Helper: JSONStringified', () => { + it('returns a valid JSON', () => { // Prepare const data = { body: JSON.stringify(structuredClone(basePayload)), @@ -51,7 +51,7 @@ describe('JSONStringified', () => { }); }); - it('should throw an error if the JSON payload is invalid', () => { + it('throws an error if the JSON payload is invalid', () => { // Prepare const data = { body: JSON.stringify({ ...basePayload, email: 'invalid' }), @@ -66,7 +66,7 @@ describe('JSONStringified', () => { expect(() => extendedSchema.parse(data)).toThrow(); }); - it('should throw an error if the JSON is malformed', () => { + it('throws an error if the JSON is malformed', () => { // Prepare const data = { body: 'invalid', @@ -81,11 +81,11 @@ describe('JSONStringified', () => { expect(() => extendedSchema.parse(data)).toThrow(); }); - it('should parse extended AlbSchema', () => { + it('parses extended AlbSchema', () => { // Prepare const testEvent = getTestEvent({ - eventsPath: '.', - filename: 'albEvent', + eventsPath: 'alb', + filename: 'base', }); testEvent.body = JSON.stringify(structuredClone(basePayload)); @@ -101,7 +101,7 @@ describe('JSONStringified', () => { }); }); - it('should parse extended SqsSchema', () => { + it('parses extended SqsSchema', () => { // Prepare const testEvent = getTestEvent({ eventsPath: 'sqs', @@ -130,7 +130,7 @@ describe('JSONStringified', () => { }); }); - it('should parse extended SnsSchema', () => { + it('parses extended SnsSchema', () => { // Prepare const testEvent = getTestEvent({ eventsPath: 'sns', @@ -162,7 +162,7 @@ describe('JSONStringified', () => { }); }); -describe('DynamoDBMarshalled', () => { +describe('Helper: DynamoDBMarshalled', () => { // Prepare const schema = z.object({ Message: z.string(), @@ -184,115 +184,96 @@ describe('DynamoDBMarshalled', () => { ), }); - it('should correctly unmarshall and validate a valid DynamoDB stream record', () => { + it('unmarshalls and validates a valid DynamoDB stream record', () => { // Prepare - const testInput = [ - { - Message: { - S: 'New item!', - }, - Id: { - N: '101', - }, + const event = structuredClone(baseEvent); + event.Records[0].dynamodb.NewImage = { + Message: { + S: 'New item!', }, - { - Message: { - S: 'This item has changed', - }, - Id: { - N: '101', - }, + Id: { + N: '101', }, - ]; - const expectedOutput = [ - { - Id: 101, - Message: 'New item!', + }; + event.Records[1].dynamodb.NewImage = { + Message: { + S: 'This item has changed', }, - { - Id: 101, - Message: 'This item has changed', + Id: { + N: '101', }, - ]; - - const testEvent = structuredClone(baseEvent); - testEvent.Records[0].dynamodb.NewImage = testInput[0]; - testEvent.Records[1].dynamodb.NewImage = testInput[1]; + }; // Act & Assess - expect(extendedSchema.parse(testEvent)).toStrictEqual({ + expect(extendedSchema.parse(event)).toStrictEqual({ Records: [ { - ...testEvent.Records[0], + ...event.Records[0], dynamodb: { - NewImage: expectedOutput[0], + NewImage: { + Id: 101, + Message: 'New item!', + }, }, }, { - ...testEvent.Records[1], + ...event.Records[1], dynamodb: { - NewImage: expectedOutput[1], + NewImage: { + Id: 101, + Message: 'This item has changed', + }, }, }, ], }); }); - it('should throw an error if the DynamoDB stream record cannot be unmarshalled', () => { + it('throws an error if the DynamoDB stream record cannot be unmarshalled', () => { // Prepare - const testInput = [ - { - Message: { - S: 'New item!', - }, - Id: { - NNN: '101', //unknown type - }, + const event = structuredClone(baseEvent); + event.Records[0].dynamodb.NewImage = { + Message: { + S: 'New item!', }, - { - Message: { - S: 'This item has changed', - }, - Id: { - N: '101', - }, + Id: { + NNN: '101', //unknown type }, - ]; - - const testEvent = structuredClone(baseEvent); - testEvent.Records[0].dynamodb.NewImage = testInput[0]; - testEvent.Records[1].dynamodb.NewImage = testInput[1]; + }; + event.Records[1].dynamodb.NewImage = { + Message: { + S: 'This item has changed', + }, + Id: { + N: '101', + }, + }; // Act & Assess - expect(() => extendedSchema.parse(testEvent)).toThrow( + expect(() => extendedSchema.parse(event)).toThrow( 'Could not unmarshall DynamoDB stream record' ); }); - it('should throw a validation error if the unmarshalled record does not match the schema', () => { + it('throws a validation error if the unmarshalled record does not match the schema', () => { // Prepare - const testInput = [ - { - Message: { - S: 'New item!', - }, - Id: { - N: '101', - }, + const event = structuredClone(baseEvent); + event.Records[0].dynamodb.NewImage = { + Message: { + S: 'New item!', }, - { - Message: { - S: 'This item has changed', - }, - // Id is missing + Id: { + N: '101', }, - ]; - - const testEvent = structuredClone(baseEvent); - testEvent.Records[0].dynamodb.NewImage = testInput[0]; - testEvent.Records[1].dynamodb.NewImage = testInput[1]; + }; + event.Records[1].dynamodb.NewImage = { + // Id is missing + Message: { + S: 'This item has changed', + }, + }; // Act & Assess - expect(() => extendedSchema.parse(testEvent)).toThrow(); + expect(() => extendedSchema.parse(event)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/schema/alb.test.ts b/packages/parser/tests/unit/schema/alb.test.ts index 2e71f92438..01cea17b76 100644 --- a/packages/parser/tests/unit/schema/alb.test.ts +++ b/packages/parser/tests/unit/schema/alb.test.ts @@ -1,23 +1,60 @@ import { describe, expect, it } from 'vitest'; -import { AlbMultiValueHeadersSchema, AlbSchema } from '../../../src/schemas/'; -import { TestEvents } from './utils.js'; +import { AlbSchema } from '../../../src/schemas/alb.js'; +import type { ALBEvent } from '../../../src/types/schema.js'; +import { getTestEvent, omit } from './utils.js'; -describe('ALB ', () => { - it('should parse alb event', () => { - const albEvent = TestEvents.albEvent; - expect(AlbSchema.parse(albEvent)).toEqual(albEvent); +describe('Schema: ALB', () => { + const eventsPath = 'alb'; + const baseEvent = getTestEvent({ + eventsPath, + filename: 'base', }); - it('should parse alb event path trailing slash', () => { - const albEventPathTrailingSlash = TestEvents.albEventPathTrailingSlash; - expect(AlbSchema.parse(albEventPathTrailingSlash)).toEqual( - albEventPathTrailingSlash - ); + + it('parses an ALB event', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = AlbSchema.parse(event); + + // Assess + expect(result).toStrictEqual(event); + }); + + it('parses an ALB event with a base64 encoded body', () => { + // Prepare + const event = structuredClone(baseEvent); + event.body = 'aGVsbG8gd29ybGQ='; // base64 encoded 'hello world' + // @ts-expect-error - we know the headers exist + event.headers['content-type'] = 'application/octet-stream'; + event.isBase64Encoded = true; + + // Act + const result = AlbSchema.parse(event); + + // Assess + expect(result).toStrictEqual(event); + }); + + it('parses an ALB event with multi-value headers and query string parameters', () => { + // Prepare + const event = getTestEvent({ + eventsPath, + filename: 'multi-fields', + }); + + // Act + const result = AlbSchema.parse(event); + + // Assess + expect(result).toStrictEqual(event); }); - it('should parse alb event with multi value headers event', () => { - const albMultiValueHeadersEvent = TestEvents.albMultiValueHeadersEvent; - expect(AlbMultiValueHeadersSchema.parse(albMultiValueHeadersEvent)).toEqual( - albMultiValueHeadersEvent - ); + it('throws if the event is not an ALB event', () => { + // Prepare + const event = omit(['path'], structuredClone(baseEvent)); + + // Act & Assess + expect(() => AlbSchema.parse(event)).toThrow(); }); });