diff --git a/packages/parser/src/envelopes/lambda.ts b/packages/parser/src/envelopes/lambda.ts index 91d609646f..d68af29a88 100644 --- a/packages/parser/src/envelopes/lambda.ts +++ b/packages/parser/src/envelopes/lambda.ts @@ -2,7 +2,7 @@ import type { ZodSchema, z } from 'zod'; import { ParseError } from '../errors.js'; import { LambdaFunctionUrlSchema } from '../schemas/index.js'; import type { ParsedResult } from '../types/index.js'; -import { Envelope, envelopeDiscriminator } from './envelope.js'; +import { envelopeDiscriminator } from './envelope.js'; /** * Lambda function URL envelope to extract data within body key @@ -14,37 +14,38 @@ export const LambdaFunctionUrlEnvelope = { */ [envelopeDiscriminator]: 'object' as const, parse(data: unknown, schema: T): z.infer { - const parsedEnvelope = LambdaFunctionUrlSchema.parse(data); - - if (!parsedEnvelope.body) { - throw new Error('Body field of Lambda function URL event is undefined'); + try { + return LambdaFunctionUrlSchema.extend({ + body: schema, + }).parse(data).body; + } catch (error) { + throw new ParseError('Failed to parse Lambda function URL body', { + cause: error as Error, + }); } - - return Envelope.parse(parsedEnvelope.body, schema); }, - safeParse(data: unknown, schema: T): ParsedResult> { - const parsedEnvelope = LambdaFunctionUrlSchema.safeParse(data); - - if (!parsedEnvelope.success) { - return { - success: false, - error: new ParseError('Failed to parse Lambda function URL envelope'), - originalEvent: data, - }; - } + safeParse( + data: unknown, + schema: T + ): ParsedResult> { + const results = LambdaFunctionUrlSchema.extend({ + body: schema, + }).safeParse(data); - const parsedBody = Envelope.safeParse(parsedEnvelope.data.body, schema); - if (!parsedBody.success) { + if (!results.success) { return { success: false, error: new ParseError('Failed to parse Lambda function URL body', { - cause: parsedBody.error, + cause: results.error, }), originalEvent: data, }; } - return parsedBody; + return { + success: true, + data: results.data.body, + }; }, }; diff --git a/packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json b/packages/parser/tests/events/lambda/base.json similarity index 93% rename from packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json rename to packages/parser/tests/events/lambda/base.json index 54f6560655..33a516428f 100644 --- a/packages/parser/tests/events/lambdaFunctionUrlEventPathTrailingSlash.json +++ b/packages/parser/tests/events/lambda/base.json @@ -1,7 +1,7 @@ { "version": "2.0", "routeKey": "$default", - "rawPath": "/my/path/", + "rawPath": "/", "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", "cookies": ["cookie1", "cookie2"], "headers": { @@ -31,7 +31,7 @@ "domainPrefix": "", "http": { "method": "POST", - "path": "/my/path", + "path": "/", "protocol": "HTTP/1.1", "sourceIp": "123.123.123.123", "userAgent": "agent" @@ -42,7 +42,7 @@ "time": "12/Mar/2020:19:03:58 +0000", "timeEpoch": 1583348638390 }, - "body": "Hello from client!", + "body": null, "pathParameters": null, "isBase64Encoded": false, "stageVariables": null diff --git a/packages/parser/tests/events/lambdaFunctionUrlEvent.json b/packages/parser/tests/events/lambda/get-request.json similarity index 100% rename from packages/parser/tests/events/lambdaFunctionUrlEvent.json rename to packages/parser/tests/events/lambda/get-request.json diff --git a/packages/parser/tests/events/lambdaFunctionUrlIAMEvent.json b/packages/parser/tests/events/lambda/iam-auth.json similarity index 100% rename from packages/parser/tests/events/lambdaFunctionUrlIAMEvent.json rename to packages/parser/tests/events/lambda/iam-auth.json diff --git a/packages/parser/tests/events/lambda/invalid.json b/packages/parser/tests/events/lambda/invalid.json new file mode 100644 index 0000000000..2832ef9f1a --- /dev/null +++ b/packages/parser/tests/events/lambda/invalid.json @@ -0,0 +1,48 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": null, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": null, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "http": { + "method": "POST", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": null, + "pathParameters": null, + "isBase64Encoded": false, + "stageVariables": null +} diff --git a/packages/parser/tests/unit/envelopes/lambda.test.ts b/packages/parser/tests/unit/envelopes/lambda.test.ts index b76228bb6e..eee9ff28e8 100644 --- a/packages/parser/tests/unit/envelopes/lambda.test.ts +++ b/packages/parser/tests/unit/envelopes/lambda.test.ts @@ -1,98 +1,139 @@ -import { generateMock } from '@anatine/zod-mock'; -import type { - APIGatewayProxyEventV2, - LambdaFunctionURLEvent, -} from 'aws-lambda'; import { describe, expect, it } from 'vitest'; -import { ZodError } from 'zod'; +import { ZodError, z } from 'zod'; import { ParseError } from '../../../src'; import { LambdaFunctionUrlEnvelope } from '../../../src/envelopes/index.js'; -import { TestEvents, TestSchema } from '../schema/utils.js'; +import { JSONStringified } from '../../../src/helpers'; +import type { LambdaFunctionUrlEvent } from '../../../src/types'; +import { getTestEvent, omit } from '../schema/utils.js'; -describe('Lambda Functions Url ', () => { - describe('parse', () => { - it('should parse custom schema in envelope', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; - const data = generateMock(TestSchema); +describe('Envelope: Lambda function URL', () => { + const schema = z + .object({ + message: z.string(), + }) + .strict(); - testEvent.body = JSON.stringify(data); + const baseEvent = getTestEvent({ + eventsPath: 'lambda', + filename: 'base', + }); + + describe('Method: parse', () => { + it('throws if the payload does not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); - expect(LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema)).toEqual( - data + // Act & Assess + expect(() => LambdaFunctionUrlEnvelope.parse(event, schema)).toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Failed to parse Lambda function URL body' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'object', + received: 'null', + path: ['body'], + message: 'Expected object, received null', + }, + ], + }), + }) ); }); - it('should throw when no body provided', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as LambdaFunctionURLEvent; - testEvent.body = undefined; + it('parses a Lambda function URL event with plain text', () => { + // Prepare + const event = structuredClone(baseEvent); + event.body = 'hello world'; - expect(() => - LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) - ).toThrow(); + // Act + const result = LambdaFunctionUrlEnvelope.parse(event, z.string()); + + // Assess + expect(result).toEqual('hello world'); }); - it('should throw when envelope is not valid', () => { - expect(() => - LambdaFunctionUrlEnvelope.parse({ foo: 'bar' }, TestSchema) - ).toThrow(); + it('parses a Lambda function URL event with JSON-stringified body', () => { + // Prepare + const event = structuredClone(baseEvent); + event.body = JSON.stringify({ message: 'hello world' }); + + // Act + const result = LambdaFunctionUrlEnvelope.parse( + event, + JSONStringified(schema) + ); + + // Assess + expect(result).toEqual({ message: 'hello world' }); }); - it('should throw when body does not match schema', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; - testEvent.body = JSON.stringify({ foo: 'bar' }); + it('parses a Lambda function URL event with binary body', () => { + // Prepare + const event = structuredClone(baseEvent); + event.body = Buffer.from('hello world').toString('base64'); + event.headers['content-type'] = 'application/octet-stream'; + event.isBase64Encoded = true; + + // Act + const result = LambdaFunctionUrlEnvelope.parse(event, z.string()); - expect(() => - LambdaFunctionUrlEnvelope.parse(testEvent, TestSchema) - ).toThrow(); + // Assess + expect(result).toEqual('aGVsbG8gd29ybGQ='); }); }); - describe('safeParse', () => { - it('should parse custom schema in envelope', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; - const data = generateMock(TestSchema); + describe('Method: safeParse', () => { + it('parses Lambda function URL event', () => { + // Prepare + const event = structuredClone(baseEvent); + event.body = JSON.stringify({ message: 'hello world' }); - testEvent.body = JSON.stringify(data); + // Act + const result = LambdaFunctionUrlEnvelope.safeParse( + event, + JSONStringified(schema) + ); - expect( - LambdaFunctionUrlEnvelope.safeParse(testEvent, TestSchema) - ).toEqual({ + // Assess + expect(result).toEqual({ success: true, - data, + data: { message: 'hello world' }, }); }); - it('should return original event when envelope is not valid', () => { - expect( - LambdaFunctionUrlEnvelope.safeParse({ foo: 'bar' }, TestSchema) - ).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: { foo: 'bar' }, - }); - }); + it('returns an error when the event is not valid', () => { + // Prepare + const event = omit(['rawPath'], structuredClone(baseEvent)); - it('should return original event when body does not match schema', () => { - const testEvent = - TestEvents.lambdaFunctionUrlEvent as APIGatewayProxyEventV2; - testEvent.body = JSON.stringify({ foo: 'bar' }); + // Act + const result = LambdaFunctionUrlEnvelope.safeParse(event, schema); - const parseResult = LambdaFunctionUrlEnvelope.safeParse( - testEvent, - TestSchema - ); - expect(parseResult).toEqual({ + // Assess + expect(result).toEqual({ success: false, - error: expect.any(ParseError), - originalEvent: testEvent, + error: new ParseError('Failed to parse Lambda function URL body', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['rawPath'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'object', + received: 'null', + path: ['body'], + message: 'Expected object, received null', + }, + ]), + }), + originalEvent: event, }); - - if (!parseResult.success && parseResult.error) { - expect(parseResult.error.cause).toBeInstanceOf(ZodError); - } }); }); }); diff --git a/packages/parser/tests/unit/schema/lambda.test.ts b/packages/parser/tests/unit/schema/lambda.test.ts index c5fd260112..42adf84ff8 100644 --- a/packages/parser/tests/unit/schema/lambda.test.ts +++ b/packages/parser/tests/unit/schema/lambda.test.ts @@ -1,19 +1,37 @@ import { describe, expect, it } from 'vitest'; import { LambdaFunctionUrlSchema } from '../../../src/schemas/'; -import { TestEvents } from './utils.js'; +import { getTestEvent } from './utils.js'; -describe('Lambda ', () => { - it('should parse lambda event', () => { - const lambdaFunctionUrlEvent = TestEvents.lambdaFunctionUrlEvent; +describe('Schema: LambdaFunctionUrl', () => { + const eventsPath = 'lambda'; - expect(LambdaFunctionUrlSchema.parse(lambdaFunctionUrlEvent)).toEqual( - lambdaFunctionUrlEvent - ); + it('throw when the event is invalid', () => { + // Prepare + const event = getTestEvent({ eventsPath, filename: 'invalid' }); + + // Act & Assess + expect(() => LambdaFunctionUrlSchema.parse(event)).toThrow(); + }); + + it('parses a valid event', () => { + // Prepare + const event = getTestEvent({ eventsPath, filename: 'get-request' }); + + // Act + const parsedEvent = LambdaFunctionUrlSchema.parse(event); + + // Assess + expect(parsedEvent).toEqual(event); }); - it('should parse url IAM event', () => { - const urlIAMEvent = TestEvents.lambdaFunctionUrlIAMEvent; + it('parses iam event', () => { + // Prepare + const event = getTestEvent({ eventsPath, filename: 'iam-auth' }); + + // Act + const parsedEvent = LambdaFunctionUrlSchema.parse(event); - expect(LambdaFunctionUrlSchema.parse(urlIAMEvent)).toEqual(urlIAMEvent); + // + expect(parsedEvent).toEqual(event); }); });