diff --git a/packages/parser/src/envelopes/index.ts b/packages/parser/src/envelopes/index.ts index 6ab0522ecd..40932efac5 100644 --- a/packages/parser/src/envelopes/index.ts +++ b/packages/parser/src/envelopes/index.ts @@ -8,6 +8,7 @@ export { KinesisEnvelope } from './kinesis.js'; export { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; export { LambdaFunctionUrlEnvelope } from './lambda.js'; export { SnsEnvelope } from './sns.js'; -export { SqsEnvelope, SnsSqsEnvelope } from './sqs.js'; +export { SqsEnvelope } from './sqs.js'; +export { SnsSqsEnvelope } from './snssqs.js'; export { VpcLatticeEnvelope } from './vpc-lattice.js'; export { VpcLatticeV2Envelope } from './vpc-latticev2.js'; diff --git a/packages/parser/src/envelopes/snssqs.ts b/packages/parser/src/envelopes/snssqs.ts new file mode 100644 index 0000000000..a75a709f31 --- /dev/null +++ b/packages/parser/src/envelopes/snssqs.ts @@ -0,0 +1,185 @@ +import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; +import { ParseError } from '../errors.js'; +import { SnsSqsNotificationSchema } from '../schemas/sns.js'; +import { SqsSchema } from '../schemas/sqs.js'; +import type { ParsedResult, SnsSqsNotification } from '../types/index.js'; +import { envelopeDiscriminator } from './envelope.js'; + +const createError = (index: number, issues: ZodIssue[]) => ({ + issues: issues.map((issue) => ({ + ...issue, + path: ['Records', index, 'body', ...issue.path], + })), +}); + +type ParseStepSuccess = { + success: true; + data: T; +}; + +type ParseStepError = { + success: false; + error: { issues: ZodIssue[] }; +}; + +type ParseStepResult = ParseStepSuccess | ParseStepError; + +const parseStep = ( + parser: (data: unknown) => z.SafeParseReturnType, + data: unknown, + index: number +): ParseStepResult => { + const result = parser(data); + return result.success + ? { success: true, data: result.data } + : { + success: false, + error: createError(index, result.error.issues), + }; +}; + +/** + * SNS plus SQS Envelope to extract array of Records + * + * Published messages from SNS to SQS has a slightly different payload structure + * than regular SNS messages, and when sent to SQS, they are stringified into the + * `body` field of each SQS record. + * + * To parse the `Message` field of the SNS notification, we need to: + * 1. Parse SQS schema with incoming data + * 2. `JSON.parse()` the SNS payload and parse against SNS Notification schema + * 3. Finally, parse the payload against the provided schema + */ +export const SnsSqsEnvelope = { + /** + * This is a discriminator to differentiate whether an envelope returns an array or an object + * @hidden + */ + [envelopeDiscriminator]: 'array' as const, + parse(data: unknown, schema: T): z.infer[] { + let parsedEnvelope: z.infer; + try { + parsedEnvelope = SqsSchema.parse(data); + } catch (error) { + throw new ParseError('Failed to parse SQS Envelope', { + cause: error as Error, + }); + } + + return parsedEnvelope.Records.map((record, recordIndex) => { + try { + return schema.parse( + SnsSqsNotificationSchema.parse(JSON.parse(record.body)).Message + ); + } catch (error) { + throw new ParseError( + `Failed to parse SQS Record at index ${recordIndex}`, + { + cause: new ZodError( + error instanceof ZodError + ? (error as ZodError).issues.map((issue) => ({ + ...issue, + path: ['Records', recordIndex, 'body', ...issue.path], + })) + : [ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', recordIndex, 'body'], + }, + ] + ), + } + ); + } + }); + }, + + safeParse( + data: unknown, + schema: T + ): ParsedResult[]> { + const parsedEnvelope = SqsSchema.safeParse(data); + if (!parsedEnvelope.success) { + return { + success: false, + error: new ParseError('Failed to parse SQS envelope', { + cause: parsedEnvelope.error, + }), + originalEvent: data, + }; + } + + const parseRecord = ( + record: { body: string }, + index: number + ): ParseStepResult> => { + try { + const body = JSON.parse(record.body); + const notification = parseStep( + (data) => SnsSqsNotificationSchema.safeParse(data), + body, + index + ); + if (!notification.success) return notification; + + return parseStep>( + (data) => schema.safeParse(data), + notification.data.Message, + index + ); + } catch { + return { + success: false, + error: createError(index, [ + { + code: 'custom', + message: 'Invalid JSON', + path: [], + }, + ]), + }; + } + }; + + const result = parsedEnvelope.data.Records.reduce<{ + success: boolean; + records: z.infer[]; + errors: { + [key: number | string]: { issues: ZodIssue[] }; + }; + }>( + (acc, record, index) => { + const parsed = parseRecord(record, index); + if (!parsed.success) { + acc.success = false; + acc.errors[index] = parsed.error; + } else { + acc.records.push(parsed.data); + } + return acc; + }, + { success: true, records: [], errors: {} } + ); + + if (result.success) { + return { success: true, data: result.records }; + } + + const indexes = Object.keys(result.errors); + const errorMessage = + indexes.length > 1 + ? `Failed to parse SQS Records at indexes ${indexes.join(', ')}` + : `Failed to parse SQS Record at index ${indexes[0]}`; + + return { + success: false, + error: new ParseError(errorMessage, { + cause: new ZodError( + Object.values(result.errors).flatMap((e) => e.issues) + ), + }), + originalEvent: data, + }; + }, +}; diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index 6ba651a4f5..6d57606f7b 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -1,93 +1,62 @@ -import type { ZodSchema, z } from 'zod'; +import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; import { ParseError } from '../errors.js'; -import { SnsSqsNotificationSchema } from '../schemas/sns.js'; -import { SqsSchema } from '../schemas/sqs.js'; +import { type SqsRecordSchema, SqsSchema } from '../schemas/sqs.js'; import type { ParsedResult } from '../types/index.js'; -import { Envelope, envelopeDiscriminator } from './envelope.js'; +import { envelopeDiscriminator } from './envelope.js'; /** - * SQS Envelope to extract array of Records + * SQS Envelope to extract array of Records * - * The record's body parameter is a string, though it can also be a JSON encoded string. - * Regardless of its type it'll be parsed into a BaseModel object. + * The record's `body` parameter is a string and needs to be parsed against the provided schema. * - * Note: Records will be parsed the same way so if model is str, - * all items in the list will be parsed as str and npt as JSON (and vice versa) - */ -export const SqsEnvelope = { - /** - * This is a discriminator to differentiate whether an envelope returns an array or an object - * @hidden - */ - [envelopeDiscriminator]: 'array' as const, - parse(data: unknown, schema: T): z.infer[] { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - return Envelope.parse(record.body, schema); - }); - }, - - safeParse( - data: unknown, - schema: T - ): ParsedResult[]> { - const parsedEnvelope = SqsSchema.safeParse(data); - if (!parsedEnvelope.success) { - return { - success: false, - error: new ParseError('Failed to parse Sqs Envelope', { - cause: parsedEnvelope.error, - }), - originalEvent: data, - }; - } - - const parsedRecords: z.infer[] = []; - for (const record of parsedEnvelope.data.Records) { - const parsedRecord = Envelope.safeParse(record.body, schema); - if (!parsedRecord.success) { - return { - success: false, - error: new ParseError('Failed to parse Sqs Record', { - cause: parsedRecord.error, - }), - originalEvent: data, - }; - } - parsedRecords.push(parsedRecord.data); - } - - return { success: true, data: parsedRecords }; - }, -}; - -/** - * SNS plus SQS Envelope to extract array of Records + * If you know that the `body` is a JSON string, you can use `JSONStringified` to parse it, + * for example: * - * Published messages from SNS to SQS has a slightly different payload. - * Since SNS payload is marshalled into `Record` key in SQS, we have to: + * ```ts + * import { JSONStringified } from '@aws-lambda-powertools/helpers'; + * import { SqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs'; * - * 1. Parse SQS schema with incoming data - * 2. Unmarshall SNS payload and parse against SNS Notification schema not SNS/SNS Record - * 3. Finally, parse provided model against payload extracted + * const schema = z.object({ + * name: z.string(), + * }); * + * const parsed = SqsEnvelope.parse(event, JSONStringified(schema)); + * ``` */ -export const SnsSqsEnvelope = { +const SqsEnvelope = { /** * This is a discriminator to differentiate whether an envelope returns an array or an object * @hidden */ [envelopeDiscriminator]: 'array' as const, parse(data: unknown, schema: T): z.infer[] { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - const snsNotification = SnsSqsNotificationSchema.parse( - JSON.parse(record.body) - ); + let parsedEnvelope: z.infer; + try { + parsedEnvelope = SqsSchema.parse(data); + } catch (error) { + throw new ParseError('Failed to parse SQS Envelope', { + cause: error as Error, + }); + } - return Envelope.parse(snsNotification.Message, schema); + return parsedEnvelope.Records.map((record, recordIndex) => { + let parsedRecord: z.infer; + try { + parsedRecord = schema.parse(record.body); + } catch (error) { + throw new ParseError( + `Failed to parse SQS Record at index ${recordIndex}`, + { + cause: new ZodError( + (error as ZodError).issues.map((issue) => ({ + ...issue, + path: ['Records', recordIndex, 'body', ...issue.path], + })) + ), + } + ); + } + return parsedRecord; }); }, @@ -106,46 +75,50 @@ export const SnsSqsEnvelope = { }; } - const parsedMessages: z.infer[] = []; + const result = parsedEnvelope.data.Records.reduce<{ + success: boolean; + records: z.infer[]; + errors: { index?: number; issues?: ZodIssue[] }; + }>( + (acc, record, index) => { + const parsedRecord = schema.safeParse(record.body); - // JSON.parse can throw an error, thus we catch it and return ParsedErrorResult - try { - for (const record of parsedEnvelope.data.Records) { - const snsNotification = SnsSqsNotificationSchema.safeParse( - JSON.parse(record.body) - ); - if (!snsNotification.success) { - return { - success: false, - error: new ParseError('Failed to parse SNS notification', { - cause: snsNotification.error, - }), - originalEvent: data, - }; - } - const parsedMessage = Envelope.safeParse( - snsNotification.data.Message, - schema - ); - if (!parsedMessage.success) { - return { - success: false, - error: new ParseError('Failed to parse SNS message', { - cause: parsedMessage.error, - }), - originalEvent: data, - }; + if (!parsedRecord.success) { + const issues = parsedRecord.error.issues.map((issue) => ({ + ...issue, + path: ['Records', index, 'body', ...issue.path], + })); + acc.success = false; + // @ts-expect-error - index is assigned + acc.errors[index] = { issues }; + return acc; } - parsedMessages.push(parsedMessage.data); - } - } catch (e) { - return { - success: false, - error: e as Error, - originalEvent: data, - }; + + acc.records.push(parsedRecord.data); + return acc; + }, + { success: true, records: [], errors: {} } + ); + + if (result.success) { + return { success: true, data: result.records }; } - return { success: true, data: parsedMessages }; + const errorMessage = + Object.keys(result.errors).length > 1 + ? `Failed to parse SQS Records at indexes ${Object.keys(result.errors).join(', ')}` + : `Failed to parse SQS Record at index ${Object.keys(result.errors)[0]}`; + return { + success: false, + error: new ParseError(errorMessage, { + cause: new ZodError( + // @ts-expect-error - issues are assigned because success is false + Object.values(result.errors).flatMap((error) => error.issues) + ), + }), + originalEvent: data, + }; }, }; + +export { SqsEnvelope }; diff --git a/packages/parser/src/schemas/sqs.ts b/packages/parser/src/schemas/sqs.ts index 42aa348d30..f3c8ecd11b 100644 --- a/packages/parser/src/schemas/sqs.ts +++ b/packages/parser/src/schemas/sqs.ts @@ -93,7 +93,7 @@ const SqsRecordSchema = z.object({ * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#example-standard-queue-message-event} */ const SqsSchema = z.object({ - Records: z.array(SqsRecordSchema), + Records: z.array(SqsRecordSchema).min(1), }); export { SqsSchema, SqsRecordSchema }; diff --git a/packages/parser/tests/events/snsSqsFifoEvent.json b/packages/parser/tests/events/snsSqsFifoEvent.json deleted file mode 100644 index 52e45ce24e..0000000000 --- a/packages/parser/tests/events/snsSqsFifoEvent.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Records": [ - { - "messageId": "69bc4bbd-ed69-4325-a434-85c3b428ceab", - "receiptHandle": "AQEBbfAqjhrgIdW3HGWYPz57mdDatG/dT9LZhRPAsNQ1pJmw495w4esDc8ZSbOwMZuPBol7wtiNWug8U25GpSQDDLY1qv//8/lfmdzXOiprG6xRVeiXSHj0j731rJQ3xo+GPdGjOzjIxI09CrE3HtZ4lpXY9NjjHzP8hdxkCLlbttumc8hDBUR365/Tk+GfV2nNP9qvZtLGEbKCdTm/GYdTSoAr+ML9HnnGrS9T25Md71ASiZMI4DZqptN6g7CYYojFPs1LVM9o1258ferA72zbNoQ==", - "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"a7c9d2fa-77fa-5184-9de9-89391027cc7d\",\n \"SequenceNumber\" : \"10000000000000004000\",\n \"TopicArn\" : \"arn:aws:sns:eu-west-1:231436140809:Test.fifo\",\n \"Message\" : \"{\\\"message\\\": \\\"hello world\\\", \\\"username\\\": \\\"lessa\\\"}\",\n \"Timestamp\" : \"2022-10-14T13:35:25.419Z\",\n \"UnsubscribeURL\" : \"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:231436140809:Test.fifo:bb81d3de-a0f9-46e4-b619-d3152a4d545f\"\n}", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1665754525442", - "SequenceNumber": "18873177232222703872", - "MessageGroupId": "powertools-test", - "SenderId": "AIDAWYJAWPFU7SUQGUJC6", - "MessageDeduplicationId": "4e0a0f61eed277a4b9e4c01d5722b07b0725e42fe782102abee5711adfac701f", - "ApproximateFirstReceiveTimestamp": "1665754525442" - }, - "messageAttributes": {}, - "md5OfBody": "f3c788e623445e3feb263e80c1bffc0b", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:eu-west-1:231436140809:Test.fifo", - "awsRegion": "eu-west-1" - } - ] -} diff --git a/packages/parser/tests/events/sqsEvent.json b/packages/parser/tests/events/sqs/base.json similarity index 100% rename from packages/parser/tests/events/sqsEvent.json rename to packages/parser/tests/events/sqs/base.json diff --git a/packages/parser/tests/events/snsSqsEvent.json b/packages/parser/tests/events/sqs/sns-body.json similarity index 59% rename from packages/parser/tests/events/snsSqsEvent.json rename to packages/parser/tests/events/sqs/sns-body.json index 1162ffdb4e..79b55d19eb 100644 --- a/packages/parser/tests/events/snsSqsEvent.json +++ b/packages/parser/tests/events/sqs/sns-body.json @@ -3,7 +3,7 @@ { "messageId": "79406a00-bf15-46ca-978c-22c3613fcb30", "receiptHandle": "AQEB3fkqlBqq239bMCAHIr5mZkxJYKtxsTTy1lMImmpY7zqpQdfcAE8zFiuRh7X5ciROy24taT2rRXfuJFN/yEUVcQ6d5CIOCEK4htmRJJOHIyGdZPAm2NUUG5nNn2aEzgfzVvrkPBsrCbr7XTzK5s6eUZNH/Nn9AJtHKHpzweRK34Bon9OU/mvyIT7EJbwHPsdhL14NrCp8pLWBiIhkaJkG2G6gPO89dwHtGVUARJL+zP70AuIu/f7QgmPtY2eeE4AVbcUT1qaIlSGHUXxoHq/VMHLd/c4zWl0EXQOo/90DbyCUMejTIKL7N15YfkHoQDHprvMiAr9S75cdMiNOduiHzZLg/qVcv4kxsksKLFMKjwlzmYuQYy2KslVGwoHMd4PD", - "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"d88d4479-6ec0-54fe-b63f-1cf9df4bb16e\",\n \"TopicArn\" : \"arn:aws:sns:eu-west-1:231436140809:powertools265\",\n \"Subject\" : null,\n \"Message\" : \"{\\\"message\\\": \\\"hello world\\\", \\\"username\\\": \\\"lessa\\\"}\",\n \"Timestamp\" : \"2021-01-19T10:07:07.287Z\",\n \"SignatureVersion\" : \"1\",\n \"Signature\" : \"tEo2i6Lw6/Dr7Jdlulh0sXgnkF0idd3hqs8QZCorQpzkIWVOuu583NT0Gv0epuZD1Bo+tex6NgP5p6415yNVujGHJKnkrA9ztzXaVgFiol8rf8AFGQbmb7RsM9BqATQUJeg9nCTe0jksmWXmjxEFr8XKyyRuQBwSlRTngAvOw8jUnCe1vyYD5xPec1xpfOEGLi5BqSog+6tBtsry3oAtcENX8SV1tVuMpp6D+UrrU8xNT/5D70uRDppkPE3vq+t7rR0fVSdQRdUV9KmQD2bflA1Dyb2y37EzwJOMHDDQ82aOhj/JmPxvEAlV8RkZl6J0HIveraRy9wbNLbI7jpiOCw==\",\n \"SigningCertURL\" : \"https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-010a507c1833636cd94bdb98bd93083a.pem\",\n \"UnsubscribeURL\" : \"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:231436140809:powertools265:15189ad7-870e-40e5-a7dd-a48898cd9f86\"\n}", + "body": "{\n \"Type\" : \"Notification\",\n \"MessageId\" : \"d88d4479-6ec0-54fe-b63f-1cf9df4bb16e\",\n \"TopicArn\" : \"arn:aws:sns:eu-west-1:231436140809:powertools265\",\n \"Subject\" : null,\n \"Message\" : \"{\\\"message\\\": \\\"hello world\\\"}\",\n \"Timestamp\" : \"2021-01-19T10:07:07.287Z\",\n \"SignatureVersion\" : \"1\",\n \"Signature\" : \"tEo2i6Lw6/Dr7Jdlulh0sXgnkF0idd3hqs8QZCorQpzkIWVOuu583NT0Gv0epuZD1Bo+tex6NgP5p6415yNVujGHJKnkrA9ztzXaVgFiol8rf8AFGQbmb7RsM9BqATQUJeg9nCTe0jksmWXmjxEFr8XKyyRuQBwSlRTngAvOw8jUnCe1vyYD5xPec1xpfOEGLi5BqSog+6tBtsry3oAtcENX8SV1tVuMpp6D+UrrU8xNT/5D70uRDppkPE3vq+t7rR0fVSdQRdUV9KmQD2bflA1Dyb2y37EzwJOMHDDQ82aOhj/JmPxvEAlV8RkZl6J0HIveraRy9wbNLbI7jpiOCw==\",\n \"SigningCertURL\" : \"https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-010a507c1833636cd94bdb98bd93083a.pem\",\n \"UnsubscribeURL\" : \"https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:231436140809:powertools265:15189ad7-870e-40e5-a7dd-a48898cd9f86\"\n}", "attributes": { "ApproximateReceiveCount": "1", "SentTimestamp": "1611050827340", diff --git a/packages/parser/tests/unit/envelopes/snssqs.test.ts b/packages/parser/tests/unit/envelopes/snssqs.test.ts new file mode 100644 index 0000000000..a7986d2a09 --- /dev/null +++ b/packages/parser/tests/unit/envelopes/snssqs.test.ts @@ -0,0 +1,350 @@ +import { describe, expect, it } from 'vitest'; +import { ZodError, z } from 'zod'; +import { SnsSqsEnvelope } from '../../../src/envelopes/snssqs.js'; +import { ParseError } from '../../../src/errors.js'; +import { JSONStringified } from '../../../src/helpers.js'; +import type { SqsEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from '../schema/utils.js'; + +describe('Envelope: SnsSqsEnvelope', () => { + const schema = z + .object({ + message: z.string(), + }) + .strict(); + const baseEvent = getTestEvent({ + eventsPath: 'sqs', + filename: 'sns-body', + }); + + describe('Method: parse', () => { + it('throws if one of the payloads does not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act & Assess + expect(() => SnsSqsEnvelope.parse(event, z.number())).toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['Records', 0, 'body'], + message: 'Expected number, received string', + }, + ]), + }) + ); + }); + + it('parses an SNS notification within an SQS envelope', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = SnsSqsEnvelope.parse(event, JSONStringified(schema)); + + // Assess + expect(result).toStrictEqual([{ message: 'hello world' }]); + }); + + it('throws if the envelope is not a valid SQS event', () => { + // Prepare + const event = { + Records: [], + }; + + // Act & Assess + expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( + new ParseError('Failed to parse SQS Envelope', { + cause: new ZodError([ + { + code: 'too_small', + minimum: 1, + type: 'array', + inclusive: true, + exact: false, + message: 'Array must contain at least 1 element(s)', + path: ['Records'], + }, + ]), + }) + ); + }); + + it('throws if the SQS message is not a valid JSON string', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[0].body = 'invalid'; + + // Act & Assess + expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( + new ParseError('Failed to parse SQS Record at index 0', { + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 0, 'body'], + }, + ]), + }) + ); + }); + + it('throws if the SQS message is not a valid SNS notification', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[0].body = JSON.stringify({ invalid: 'message' }); + + // Act & Assess + expect(() => SnsSqsEnvelope.parse(event, schema)).toThrow( + new ParseError('Failed to parse SQS Record at index 0', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'TopicArn'], + message: 'Required', + }, + { + code: 'invalid_literal', + expected: 'Notification', + path: ['Records', 0, 'body', 'Type'], + message: 'Invalid literal value, expected "Notification"', + received: undefined, + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'Message'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'MessageId'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'Timestamp'], + message: 'Required', + }, + ]), + }) + ); + }); + }); + + describe('Method: safeParse', () => { + it('parses an SNS notification within an SQS envelope', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = SnsSqsEnvelope.safeParse(event, JSONStringified(schema)); + + // Assess + expect(result).toStrictEqual({ + success: true, + data: [{ message: 'hello world' }], + }); + }); + + it('returns an error if the envelope is not a valid SQS event', () => { + // Prepare + const event = { + Records: [], + }; + + // Act + const result = SnsSqsEnvelope.safeParse(event, schema); + + // Assess + expect(result).toEqual({ + success: false, + error: new ParseError('Failed to parse SQS envelope', { + cause: new ZodError([ + { + code: 'too_small', + minimum: 1, + type: 'array', + inclusive: true, + exact: false, + message: 'Array must contain at least 1 element(s)', + path: ['Records'], + }, + ]), + }), + originalEvent: event, + }); + }); + + it('returns an error if the SQS message is not a valid JSON string', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[0].body = 'invalid'; + + // Act + const result = SnsSqsEnvelope.safeParse(event, schema); + + // Assess + expect(result).toEqual({ + success: false, + error: new ParseError('Failed to parse SQS Record at index 0', { + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 0, 'body'], + }, + ]), + }), + originalEvent: event, + }); + }); + + it('returns an error if the SQS message is not a valid SNS notification', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[0].body = JSON.stringify({ invalid: 'message' }); + + // Act + const result = SnsSqsEnvelope.safeParse(event, schema); + + // Assess + expect(result).toEqual({ + success: false, + error: new ParseError('Failed to parse SQS Record at index 0', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'TopicArn'], + message: 'Required', + }, + { + code: 'invalid_literal', + expected: 'Notification', + path: ['Records', 0, 'body', 'Type'], + message: 'Invalid literal value, expected "Notification"', + received: undefined, + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'Message'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'MessageId'], + message: 'Required', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'undefined', + path: ['Records', 0, 'body', 'Timestamp'], + message: 'Required', + }, + ]), + }), + originalEvent: event, + }); + }); + + it('returns an error if one of the payloads does not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[1] = structuredClone(event.Records[0]); + const parsedBody = JSON.parse(event.Records[0].body); + const invalidSNSNotification = { + ...parsedBody, + Message: 'hello', + }; + event.Records[1].body = JSON.stringify(invalidSNSNotification, null, 2); + + // Act + const result = SnsSqsEnvelope.safeParse(event, JSONStringified(schema)); + + // Assess + expect(result).toEqual({ + success: false, + error: new ParseError('Failed to parse SQS Record at index 1', { + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 1, 'body'], + }, + ]), + }), + originalEvent: event, + }); + }); + + it('returns a combined error if multiple payloads do not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[1] = structuredClone(event.Records[0]); + const parsedBody = JSON.parse(event.Records[0].body); + event.Records[0].body = JSON.stringify( + { + ...parsedBody, + Message: 'hello', + }, + null, + 2 + ); + event.Records[1].body = JSON.stringify( + { + ...parsedBody, + Message: 'world', + }, + null, + 2 + ); + + // Act + const result = SnsSqsEnvelope.safeParse(event, z.number()); + + // Assess + expect(result).toEqual({ + success: false, + error: new ParseError('Failed to parse SQS Records at indexes 0, 1', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['Records', 0, 'body'], + message: 'Expected number, received string', + }, + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['Records', 1, 'body'], + message: 'Expected number, received string', + }, + ]), + }), + originalEvent: event, + }); + }); + }); +}); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 004d1d71f9..d21273d47b 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -1,151 +1,153 @@ -import { generateMock } from '@anatine/zod-mock'; -import type { SQSEvent } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; -import { ZodError } from 'zod'; -import { SnsSqsEnvelope, SqsEnvelope } from '../../../src/envelopes/sqs.js'; +import { ZodError, z } from 'zod'; +import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; import { ParseError } from '../../../src/errors.js'; -import { TestEvents, TestSchema } from '../schema/utils.js'; - -describe('SqsEnvelope ', () => { - describe('parse', () => { - it('should parse custom schema in envelope', () => { - const mock = generateMock(TestSchema); - - const sqsEvent = TestEvents.sqsEvent as SQSEvent; - sqsEvent.Records[0].body = JSON.stringify(mock); - sqsEvent.Records[1].body = JSON.stringify(mock); +import { JSONStringified } from '../../../src/helpers.js'; +import type { SqsEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from '../schema/utils.js'; + +describe('Envelope: SqsEnvelope', () => { + const schema = z + .object({ + message: z.string(), + }) + .strict(); + const baseEvent = getTestEvent({ + eventsPath: 'sqs', + filename: 'base', + }); - const resp = SqsEnvelope.parse(sqsEvent, TestSchema); - expect(resp).toEqual([mock, mock]); + describe('Method: parse', () => { + it('throws if one of the payloads does not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act & Assess + expect(() => SqsEnvelope.parse(event, JSONStringified(schema))).toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Failed to parse SQS Record at index 0' + ), + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 0, 'body'], + }, + ]), + }) + ); }); - it('should throw error if invalid keys for a schema', () => { - expect(() => { - SqsEnvelope.parse({ Records: [{ foo: 'bar' }] }, TestSchema); - }).toThrow(); - }); + it('parses an SQS event', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[0].body = JSON.stringify({ message: 'hello' }); - it('should throw if invalid envelope', () => { - expect(() => { - SqsEnvelope.parse({ foo: 'bar' }, TestSchema); - }).toThrow(); + // Act + const result = SqsEnvelope.parse(event, JSONStringified(schema)); + + // Assess + expect(result).toStrictEqual([{ message: 'hello' }, { message: 'foo1' }]); }); }); describe('safeParse', () => { - it('should parse custom schema in envelope', () => { - const mock = generateMock(TestSchema); + it('parses an SQS event', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[1].body = 'bar'; - const sqsEvent = TestEvents.sqsEvent as SQSEvent; - sqsEvent.Records[0].body = JSON.stringify(mock); - sqsEvent.Records[1].body = JSON.stringify(mock); + // Act + const result = SqsEnvelope.safeParse(event, z.string()); - expect(SqsEnvelope.safeParse(sqsEvent, TestSchema)).toEqual({ + // Assess + expect(result).toStrictEqual({ success: true, - data: [mock, mock], + data: ['Test message.', 'bar'], }); }); - it('should return error if event does not match schema', () => { - const sqsEvent = TestEvents.sqsEvent as SQSEvent; - sqsEvent.Records[0].body = JSON.stringify({ foo: 'bar' }); - const parseResult = SqsEnvelope.safeParse(sqsEvent, TestSchema); - expect(parseResult).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: sqsEvent, - }); + it('returns an error if the event is not a valid SQS event', () => { + // Prepare + const event = { + Records: [], + }; - if (!parseResult.success && parseResult.error) { - expect(parseResult.error.cause).toBeInstanceOf(ZodError); - } - }); + // Act + const result = SqsEnvelope.safeParse(event, z.string()); - it('should return error if envelope is invalid', () => { - expect(SqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + // Assess + expect(result).toEqual({ success: false, - error: expect.any(ParseError), - originalEvent: { foo: 'bar' }, + error: new ParseError('Failed to parse SQS envelope', { + cause: new ZodError([ + { + code: 'too_small', + minimum: 1, + type: 'array', + inclusive: true, + exact: false, + message: 'Array must contain at least 1 element(s)', + path: ['Records'], + }, + ]), + }), + originalEvent: event, }); }); - }); - - describe('SnsSqsEnvelope safeParse', () => { - it('parse sqs inside sns envelope', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - - const data = generateMock(TestSchema); - const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); - snsEvent.Message = JSON.stringify(data); - - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - expect(SnsSqsEnvelope.parse(snsSqsTestEvent, TestSchema)).toEqual([data]); - }); - - it('should parse sqs inside sns envelope', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + it('returns an error if any of the records fail to parse', () => { + // Prepare + const event = structuredClone(baseEvent); - const data = generateMock(TestSchema); - const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); - snsEvent.Message = JSON.stringify(data); + // Act + const result = SqsEnvelope.safeParse(event, JSONStringified(schema)); - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - - expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ - success: true, - data: [data], - }); - }); - it('should return error when envelope is not valid', () => { - expect(SnsSqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + // Assess + expect(result).toEqual({ success: false, - error: expect.any(ParseError), - originalEvent: { foo: 'bar' }, + error: new ParseError('Failed to parse SQS Record at index 0', { + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 0, 'body'], + }, + ]), + }), + originalEvent: event, }); }); - it('should return error if message does not match schema', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - - const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); - snsEvent.Message = JSON.stringify({ - foo: 'bar', - }); - - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); - - const parseResult = SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema); - expect(parseResult).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: snsSqsTestEvent, - }); - if (!parseResult.success && parseResult.error) { - expect(parseResult.error.cause).toBeInstanceOf(ZodError); - } - }); - it('should return error if sns message is not valid', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; - - snsSqsTestEvent.Records[0].body = JSON.stringify({ - foo: 'bar', - }); - - expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: snsSqsTestEvent, - }); - }); - it('should return error if JSON parse fails for record.body', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + it('returns a combined error if multiple records fail to parse', () => { + // Prepare + const event = structuredClone(baseEvent); - snsSqsTestEvent.Records[0].body = 'not a json string'; + // Act + const result = SqsEnvelope.safeParse(event, z.number()); - expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + // Assess + expect(result).toEqual({ success: false, - error: expect.any(SyntaxError), - originalEvent: snsSqsTestEvent, + error: new ParseError('Failed to parse SQS Records at indexes 0, 1', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['Records', 0, 'body'], + message: 'Expected number, received string', + }, + { + code: 'invalid_type', + expected: 'number', + received: 'string', + path: ['Records', 1, 'body'], + message: 'Expected number, received string', + }, + ]), + }), + originalEvent: event, }); }); }); diff --git a/packages/parser/tests/unit/helpers.test.ts b/packages/parser/tests/unit/helpers.test.ts index f8c4ce0b0a..4df9d32be1 100644 --- a/packages/parser/tests/unit/helpers.test.ts +++ b/packages/parser/tests/unit/helpers.test.ts @@ -104,8 +104,8 @@ describe('JSONStringified', () => { it('should parse extended SqsSchema', () => { // Prepare const testEvent = getTestEvent({ - eventsPath: '.', - filename: 'sqsEvent', + eventsPath: 'sqs', + filename: 'base', }); const stringifiedBody = JSON.stringify(basePayload); testEvent.Records[0].body = stringifiedBody; diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts index 805c0fd98f..50893dc10b 100644 --- a/packages/parser/tests/unit/parser.middy.test.ts +++ b/packages/parser/tests/unit/parser.middy.test.ts @@ -1,248 +1,193 @@ -import { generateMock } from '@anatine/zod-mock'; import middy from '@middy/core'; import type { Context } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; -import type { ZodSchema, z } from 'zod'; -import { ParseError } from '../../src'; -import { EventBridgeEnvelope, SqsEnvelope } from '../../src/envelopes'; +import { z } from 'zod'; +import { EventBridgeEnvelope } from '../../src/envelopes/event-bridge.js'; +import { SqsEnvelope } from '../../src/envelopes/sqs.js'; +import { ParseError } from '../../src/errors.js'; import { parser } from '../../src/middleware/parser.js'; -import { SqsSchema } from '../../src/schemas'; -import type { EventBridgeEvent, ParsedResult, SqsEvent } from '../../src/types'; -import { TestSchema, getTestEvent } from './schema/utils'; +import type { + EventBridgeEvent, + ParsedResult, + SqsEvent, +} from '../../src/types/index.js'; +import { getTestEvent } from './schema/utils.js'; describe('Middleware: parser', () => { - type TestEvent = z.infer; - const handler = async ( - event: unknown, - _context: Context - ): Promise => { - return event; - }; - - describe(' when envelope is provided ', () => { - const middyfiedHandlerSchemaEnvelope = middy() - .use(parser({ schema: TestSchema, envelope: SqsEnvelope })) - .handler(async (event, _): Promise => { - return event; - }); - it('should parse request body with schema and envelope', async () => { - const bodyMock = generateMock(TestSchema); - - const event = generateMock(SqsSchema, { - stringMap: { - body: () => JSON.stringify(bodyMock), - }, - }); - - const result = (await middyfiedHandlerSchemaEnvelope( - event as unknown as TestEvent[], - {} as Context - )) as TestEvent[]; - for (const item of result) { - expect(item).toEqual(bodyMock); - } - }); + const schema = z + .object({ + name: z.string(), + age: z.number(), + }) + .strict(); + const baseSqsEvent = getTestEvent({ + eventsPath: 'sqs', + filename: 'base', + }); + const baseEventBridgeEvent = getTestEvent({ + eventsPath: 'eventbridge', + filename: 'base', + }); + const JSONPayload = { name: 'John', age: 18 }; - it('should throw when envelope does not match', async () => { - await expect(async () => { - await middyfiedHandlerSchemaEnvelope( - { name: 'John', age: 18 } as unknown as TestEvent[], - {} as Context - ); - }).rejects.toThrow(); - }); + const handlerWithSchemaAndEnvelope = middy() + .use(parser({ schema: z.string(), envelope: SqsEnvelope })) + .handler(async (event) => event); - it('should throw when schema does not match', async () => { - const event = generateMock(SqsSchema, { - stringMap: { - body: () => '42', - }, - }); - - await expect( - middyfiedHandlerSchemaEnvelope( - event as unknown as TestEvent[], - {} as Context - ) - ).rejects.toThrow(); - }); + it('parses an event with schema and envelope', async () => { + // Prepare + const event = structuredClone(baseSqsEvent); + event.Records[1].body = 'bar'; - it('should throw when provided schema is invalid', async () => { - const middyfiedHandler = middy(handler).use( - parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) - ); + // Act + const result = await handlerWithSchemaAndEnvelope( + event as unknown as string[], + {} as Context + ); - await expect( - middyfiedHandler(42 as unknown as TestEvent[], {} as Context) - ).rejects.toThrow(); - }); - - it('should throw when envelope is correct but schema is invalid', async () => { - const event = generateMock(SqsSchema, { - stringMap: { - body: () => JSON.stringify({ name: 'John', foo: 'bar' }), - }, - }); + // Assess + expect(result).toStrictEqual(['Test message.', 'bar']); + }); - const middyfiedHandler = middy(handler).use( - parser({ schema: {} as ZodSchema, envelope: SqsEnvelope }) - ); + it('throws when envelope does not match', async () => { + // Prepare + const event = structuredClone(baseEventBridgeEvent); - await expect( - middyfiedHandler(event as unknown as TestEvent[], {} as Context) - ).rejects.toThrow(); - }); + // Act & Assess + expect( + middy() + .use(parser({ schema: z.string(), envelope: SqsEnvelope })) + .handler((event) => event)(event as unknown as string[], {} as Context) + ).rejects.toThrow(); }); - describe(' when envelope is not provided', () => { - it('should parse the event with built-in schema', async () => { - const event = generateMock(SqsSchema); - - const middyfiedHandler = middy() - .use(parser({ schema: SqsSchema })) - .handler(async (event, _) => { - return event; - }); + it('throws when schema does not match', async () => { + // Prepare + const event = structuredClone(baseSqsEvent); + // @ts-expect-error - setting an invalid body + event.Records[1].body = undefined; - expect( - await middyfiedHandler(event as unknown as SqsEvent, {} as Context) - ).toEqual(event); - }); + // Act & Assess + expect( + handlerWithSchemaAndEnvelope(event as unknown as string[], {} as Context) + ).rejects.toThrow(); + }); - it('should parse custom event', async () => { - const event = { name: 'John', age: 18 }; - const middyfiedHandler = middy() - .use(parser({ schema: TestSchema })) - .handler(async (event, _): Promise => { - return event; - }); - - expect( - await middyfiedHandler(event as unknown as TestEvent, {} as Context) - ).toEqual(event); - }); + it('parses the event successfully', async () => { + // Prepare + const event = 42; - it('should throw when the schema does not match', async () => { - const middyfiedHandler = middy(handler).use( - parser({ schema: TestSchema }) - ); + // Act + const result = await middy() + .use(parser({ schema: z.number() })) + .handler((event) => event)(event as unknown as number, {} as Context); - await expect( - middyfiedHandler(42 as unknown as TestEvent, {} as Context) - ).rejects.toThrow(); - }); + // Assess + expect(result).toEqual(event); + }); - it('should throw when provided schema is invalid', async () => { - const middyfiedHandler = middy(handler).use( - parser({ schema: {} as ZodSchema }) - ); + it('throws when the event does not match the schema', async () => { + // Prepare + const event = structuredClone(JSONPayload); - await expect( - middyfiedHandler({ foo: 'bar' } as unknown as TestEvent, {} as Context) - ).rejects.toThrow(); - }); + // Act & Assess + expect( + middy((event) => event).use(parser({ schema: z.number() }))( + event as unknown as number, + {} as Context + ) + ).rejects.toThrow(); + }); - it('should return the event when safeParse is true', async () => { - const event = { name: 'John', age: 18 }; - const middyfiedHandler = middy() - .use(parser({ schema: TestSchema, safeParse: true })) - .handler( - async (event, _): Promise> => { - return event; - } - ); - - expect( - await middyfiedHandler( - event as unknown as ParsedResult, - {} as Context - ) - ).toEqual({ - success: true, - data: event, - }); + it('returns the payload when using safeParse', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act + const result = await middy() + .use(parser({ schema: schema, safeParse: true })) + .handler((event) => event)( + event as unknown as ParsedResult>, + {} as Context + ); + + // Assess + expect(result).toEqual({ + success: true, + data: event, }); + }); - it('should return error when safeParse is true and schema does not match', async () => { - const middyfiedHandler = middy(handler).use( - parser({ schema: TestSchema, safeParse: true }) - ); - - expect( - await middyfiedHandler( - 42 as unknown as ParsedResult, - {} as Context - ) - ).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: 42, - }); + it('returns the error when using safeParse and the payload is invalid', async () => { + // Prepare + const event = structuredClone(JSONPayload); + + // Act + const result = await middy() + .use(parser({ schema: z.string(), safeParse: true })) + .handler((event) => event)( + event as unknown as ParsedResult, + {} as Context + ); + + // Assess + expect(result).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: event, }); + }); - it('should return event when envelope and safeParse are true', async () => { - const detail = generateMock(TestSchema); - const event = getTestEvent({ - eventsPath: 'eventbridge', - filename: 'base', - }); - - event.detail = detail; - - const middyfiedHandler = middy() - .use( - parser({ - schema: TestSchema, - envelope: EventBridgeEnvelope, - safeParse: true, - }) - ) - .handler( - async (event, _): Promise> => { - return event; - } - ); - - expect( - await middyfiedHandler( - event as unknown as ParsedResult, - {} as Context - ) - ).toEqual({ - success: true, - data: detail, - }); + it('returns the payload when using safeParse with envelope', async () => { + // Prepare + const detail = structuredClone(JSONPayload); + const event = structuredClone(baseEventBridgeEvent); + event.detail = detail; + + // Act + const result = await middy() + .use( + parser({ + schema: schema, + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ) + .handler((event) => event)( + event as unknown as ParsedResult>, + {} as Context + ); + + // Assess + expect(result).toStrictEqual({ + success: true, + data: detail, }); + }); - it('should return error when envelope provided, safeParse is true, and schema does not match', async () => { - const event = getTestEvent({ - eventsPath: 'eventbridge', - filename: 'base', - }); - - const middyfiedHandler = middy() - .use( - parser({ - schema: TestSchema, - envelope: EventBridgeEnvelope, - safeParse: true, - }) - ) - .handler( - async (event, _): Promise> => { - return event; - } - ); - expect( - await middyfiedHandler( - event as unknown as ParsedResult, - {} as Context - ) - ).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: event, - }); + it('returns an error when using safeParse with envelope and the payload is invalid', async () => { + // Prepare + const event = structuredClone(baseEventBridgeEvent); + + // Act + const result = await middy() + .use( + parser({ + schema: z.string(), + envelope: EventBridgeEnvelope, + safeParse: true, + }) + ) + .handler((event) => event)( + event as unknown as ParsedResult, + {} as Context + ); + + // Assess + expect(result).toStrictEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: event, }); }); }); diff --git a/packages/parser/tests/unit/schema/sqs.test.ts b/packages/parser/tests/unit/schema/sqs.test.ts index 7e52ce99ef..4fcba4c30f 100644 --- a/packages/parser/tests/unit/schema/sqs.test.ts +++ b/packages/parser/tests/unit/schema/sqs.test.ts @@ -1,17 +1,32 @@ import { describe, expect, it } from 'vitest'; -import { SqsRecordSchema, SqsSchema } from '../../../src/schemas/'; -import type { SqsEvent } from '../../../src/types'; -import type { SqsRecord } from '../../../src/types/schema'; -import { TestEvents } from './utils.js'; +import { SqsSchema } from '../../../src/schemas/sqs.js'; +import type { SqsEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from './utils.js'; -describe('SQS', () => { - it('should parse sqs event', () => { - const sqsEvent = TestEvents.sqsEvent; - expect(SqsSchema.parse(sqsEvent)).toEqual(sqsEvent); +describe('Schema: SQS', () => { + const baseEvent = getTestEvent({ + eventsPath: 'sqs', + filename: 'base', }); - it('should parse record from sqs event', () => { - const sqsEvent: SqsEvent = TestEvents.sqsEvent as SqsEvent; - const parsed: SqsRecord = SqsRecordSchema.parse(sqsEvent.Records[0]); - expect(parsed.body).toEqual('Test message.'); + + it('parses an SQS event', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = SqsSchema.parse(event); + + // Assess + expect(result).toStrictEqual(event); + }); + + it('throws if the event is not an SQS event', () => { + // Prepare + const event = { + Records: [], + }; + + // Act & Assess + expect(() => SqsSchema.parse(event)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/schema/utils.ts b/packages/parser/tests/unit/schema/utils.ts index cf1da36e99..de56e12a60 100644 --- a/packages/parser/tests/unit/schema/utils.ts +++ b/packages/parser/tests/unit/schema/utils.ts @@ -36,9 +36,6 @@ const filenames = [ 's3ObjectEventTempCredentials', 's3SqsEvent', 'sesEvent', - 'snsSqsEvent', - 'snsSqsFifoEvent', - 'sqsEvent', 'vpcLatticeEvent', 'vpcLatticeEventPathTrailingSlash', 'vpcLatticeEventV2PathTrailingSlash',