From 3dcbfa997a859301d775d9f5efdf7d33d66ad229 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 21 Jan 2025 18:30:36 +0100 Subject: [PATCH 1/2] fix(parser): SNS Envelope handles non-JSON --- packages/parser/src/envelopes/sns.ts | 161 ++++------ packages/parser/src/envelopes/sqs.ts | 89 ++++++ packages/parser/src/schemas/sns.ts | 2 +- .../events/{snsEvent.json => sns/base.json} | 6 +- .../parser/tests/unit/envelopes/sns.test.ts | 284 +++++++++--------- .../parser/tests/unit/envelopes/sqs.test.ts | 83 ++++- packages/parser/tests/unit/helpers.test.ts | 4 +- packages/parser/tests/unit/schema/sns.test.ts | 97 +++--- packages/parser/tests/unit/schema/utils.ts | 1 - 9 files changed, 435 insertions(+), 292 deletions(-) rename packages/parser/tests/events/{snsEvent.json => sns/base.json} (91%) diff --git a/packages/parser/src/envelopes/sns.ts b/packages/parser/src/envelopes/sns.ts index 183cc84470..3a7d40cb32 100644 --- a/packages/parser/src/envelopes/sns.ts +++ b/packages/parser/src/envelopes/sns.ts @@ -1,9 +1,8 @@ -import type { ZodSchema, z } from 'zod'; +import { ZodError, type ZodIssue, type ZodSchema, type z } from 'zod'; import { ParseError } from '../errors.js'; -import { SnsSchema, SnsSqsNotificationSchema } from '../schemas/sns.js'; -import { SqsSchema } from '../schemas/sqs.js'; +import { SnsSchema } from '../schemas/sns.js'; import type { ParsedResult } from '../types/index.js'; -import { Envelope, envelopeDiscriminator } from './envelope.js'; +import { envelopeDiscriminator } from './envelope.js'; /** * SNS Envelope to extract array of Records @@ -23,8 +22,19 @@ export const SnsEnvelope = { parse(data: unknown, schema: T): z.infer[] { const parsedEnvelope = SnsSchema.parse(data); - return parsedEnvelope.Records.map((record) => { - return Envelope.parse(record.Sns.Message, schema); + return parsedEnvelope.Records.map((record, index) => { + try { + return schema.parse(record.Sns.Message); + } catch (error) { + throw new ParseError(`Failed to parse SNS record at index ${index}`, { + cause: new ZodError( + (error as ZodError).issues.map((issue) => ({ + ...issue, + path: ['Records', index, 'Sns', 'Message', ...issue.path], + })) + ), + }); + } }); }, @@ -44,112 +54,47 @@ export const SnsEnvelope = { }; } - const parsedMessages: z.infer[] = []; - for (const record of parsedEnvelope.data.Records) { - const parsedMessage = Envelope.safeParse(record.Sns.Message, schema); - if (!parsedMessage.success) { - return { - success: false, - error: new ParseError('Failed to parse SNS message', { - cause: parsedMessage.error, - }), - originalEvent: data, - }; - } - parsedMessages.push(parsedMessage.data); - } - - return { - success: true, - data: parsedMessages, - }; - }, -}; - -/** - * SNS plus SQS Envelope to extract array of Records - * - * Published messages from SNS to SQS has a slightly different payload. - * Since SNS payload is marshalled into `Record` key in SQS, we have to: - * - * 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 - * - */ -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[] { - const parsedEnvelope = SqsSchema.parse(data); - - return parsedEnvelope.Records.map((record) => { - const snsNotification = SnsSqsNotificationSchema.parse( - JSON.parse(record.body) - ); + const result = parsedEnvelope.data.Records.reduce<{ + success: boolean; + messages: z.infer[]; + errors: { index?: number; issues?: ZodIssue[] }; + }>( + (acc, message, index) => { + const parsedMessage = schema.safeParse(message.Sns.Message); + if (!parsedMessage.success) { + acc.success = false; + const issues = parsedMessage.error.issues.map((issue) => ({ + ...issue, + path: ['Records', index, 'Sns', 'Message', ...issue.path], + })); + // @ts-expect-error - index is assigned + acc.errors[index] = { issues }; + return acc; + } - return Envelope.parse(snsNotification.Message, schema); - }); - }, + acc.messages.push(parsedMessage.data); + return acc; + }, + { success: true, messages: [], errors: {} } + ); - 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, - }; + if (result.success) { + return { success: true, data: result.messages }; } - const parsedMessages: z.infer[] = []; + const errorMessage = + Object.keys(result.errors).length > 1 + ? `Failed to parse SNS messages at indexes ${Object.keys(result.errors).join(', ')}` + : `Failed to parse SNS message at index ${Object.keys(result.errors)[0]}`; + const errorCause = new ZodError( + // @ts-expect-error - issues are assigned because success is false + Object.values(result.errors).flatMap((error) => error.issues) + ); - // 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, - }; - } - parsedMessages.push(parsedMessage.data); - } - } catch (e) { - return { - success: false, - error: e as Error, - originalEvent: data, - }; - } - - return { success: true, data: parsedMessages }; + return { + success: false, + error: new ParseError(errorMessage, { cause: errorCause }), + originalEvent: data, + }; }, }; diff --git a/packages/parser/src/envelopes/sqs.ts b/packages/parser/src/envelopes/sqs.ts index c7bf36a9d1..6ba651a4f5 100644 --- a/packages/parser/src/envelopes/sqs.ts +++ b/packages/parser/src/envelopes/sqs.ts @@ -1,5 +1,6 @@ import type { ZodSchema, z } from 'zod'; import { ParseError } from '../errors.js'; +import { SnsSqsNotificationSchema } from '../schemas/sns.js'; import { SqsSchema } from '../schemas/sqs.js'; import type { ParsedResult } from '../types/index.js'; import { Envelope, envelopeDiscriminator } from './envelope.js'; @@ -60,3 +61,91 @@ export const SqsEnvelope = { return { success: true, data: parsedRecords }; }, }; + +/** + * SNS plus SQS Envelope to extract array of Records + * + * Published messages from SNS to SQS has a slightly different payload. + * Since SNS payload is marshalled into `Record` key in SQS, we have to: + * + * 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 + * + */ +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[] { + const parsedEnvelope = SqsSchema.parse(data); + + return parsedEnvelope.Records.map((record) => { + const snsNotification = SnsSqsNotificationSchema.parse( + JSON.parse(record.body) + ); + + return Envelope.parse(snsNotification.Message, 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 parsedMessages: z.infer[] = []; + + // 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, + }; + } + parsedMessages.push(parsedMessage.data); + } + } catch (e) { + return { + success: false, + error: e as Error, + originalEvent: data, + }; + } + + return { success: true, data: parsedMessages }; + }, +}; diff --git a/packages/parser/src/schemas/sns.ts b/packages/parser/src/schemas/sns.ts index e13501b270..a76711e014 100644 --- a/packages/parser/src/schemas/sns.ts +++ b/packages/parser/src/schemas/sns.ts @@ -113,7 +113,7 @@ const SnsRecordSchema = z.object({ * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html#sns-sample-event} */ const SnsSchema = z.object({ - Records: z.array(SnsRecordSchema), + Records: z.array(SnsRecordSchema).min(1), }); export { diff --git a/packages/parser/tests/events/snsEvent.json b/packages/parser/tests/events/sns/base.json similarity index 91% rename from packages/parser/tests/events/snsEvent.json rename to packages/parser/tests/events/sns/base.json index e135d8d7c9..9cf3ec939a 100644 --- a/packages/parser/tests/events/snsEvent.json +++ b/packages/parser/tests/events/sns/base.json @@ -2,7 +2,7 @@ "Records": [ { "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:sns-la ...", + "EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:ExampleTopic", "EventSource": "aws:sns", "Sns": { "SignatureVersion": "1", @@ -23,9 +23,9 @@ }, "Type": "Notification", "UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe", - "TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda", + "TopicArn": "arn:aws:sns:us-east-2:123456789012:ExampleTopic", "Subject": "TestInvoke" } } ] -} +} \ No newline at end of file diff --git a/packages/parser/tests/unit/envelopes/sns.test.ts b/packages/parser/tests/unit/envelopes/sns.test.ts index ff420961c9..be3e8c7925 100644 --- a/packages/parser/tests/unit/envelopes/sns.test.ts +++ b/packages/parser/tests/unit/envelopes/sns.test.ts @@ -1,168 +1,174 @@ -import { generateMock } from '@anatine/zod-mock'; -import type { SNSEvent, SQSEvent } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; -import { ZodError, type z } from 'zod'; -import { SnsEnvelope, SnsSqsEnvelope } from '../../../src/envelopes/index.js'; +import { ZodError, z } from 'zod'; +import { SnsEnvelope } from '../../../src/envelopes/sns.js'; import { ParseError } from '../../../src/errors.js'; -import { TestEvents, TestSchema } from '../schema/utils.js'; +import { JSONStringified } from '../../../src/helpers.js'; +import type { SnsEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from '../schema/utils.js'; + +describe('Envelope: SnsEnvelope', () => { + const baseEvent = getTestEvent({ + eventsPath: 'sns', + filename: 'base', + }); -describe('Sns and SQS Envelope', () => { - describe('SnsSqsEnvelope parse', () => { - it('should parse sqs inside sns envelope', () => { - const snsSqsTestEvent = TestEvents.snsSqsEvent as SQSEvent; + describe('Method: parse', () => { + it('throws if one of the payloads does not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act & Assess + expect(() => + SnsEnvelope.parse( + event, + z + .object({ + Message: z.string(), + }) + .strict() + ) + ).toThrow( + expect.objectContaining({ + message: expect.stringContaining( + 'Failed to parse SNS record at index 0' + ), + cause: expect.objectContaining({ + issues: [ + { + code: 'invalid_type', + expected: 'object', + received: 'string', + path: ['Records', 0, 'Sns', 'Message'], + message: 'Expected object, received string', + }, + ], + }), + }) + ); + }); - const data = generateMock(TestSchema); - const snsEvent = JSON.parse(snsSqsTestEvent.Records[0].body); - snsEvent.Message = JSON.stringify(data); + it('parses a SNS event', () => { + // Prepare + const testEvent = structuredClone(baseEvent); - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + // Act + const result = SnsEnvelope.parse(testEvent, z.string()); - expect(SnsSqsEnvelope.parse(snsSqsTestEvent, TestSchema)).toEqual([data]); + // Assess + expect(result).toStrictEqual(['Hello from SNS!']); }); }); - describe('SnsSqsEnvelope safeParse', () => { - it('should 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); + describe('Method: safeParse', () => { + it('parses a SNS event', () => { + // Prepare + const testEvent = structuredClone(baseEvent); - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + // Act + const result = SnsEnvelope.safeParse(testEvent, z.string()); - expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + // Assess + expect(result).toStrictEqual({ success: true, - data: [data], - }); - }); - it('should return error when envelope is not valid', () => { - expect(SnsSqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: { foo: 'bar' }, + data: ['Hello from SNS!'], }); }); - 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', - }); + it('returns an error if the event is not a valid SNS event', () => { + // Prepare + const event = structuredClone(baseEvent); + // @ts-expect-error - force invalid event + event.Records[0].Sns = undefined; - snsSqsTestEvent.Records[0].body = JSON.stringify(snsEvent); + // Act + const result = SnsEnvelope.safeParse(event, z.string()); - const parseResult = SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema); - expect(parseResult).toEqual({ + // Assess + expect(result).toStrictEqual({ success: false, - error: expect.any(ParseError), - originalEvent: snsSqsTestEvent, + error: new ParseError('Failed to parse SNS envelope', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'object', + received: 'undefined', + path: ['Records', 0, 'Sns'], + message: 'Required', + }, + ]), + }), + originalEvent: event, }); - - 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({ + it('returns an error if any of the messages do not match the schema', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[1] = structuredClone(event.Records[0]); + event.Records[0].Sns.Message = JSON.stringify({ foo: 'bar' }); + event.Records[1].Sns.Message = JSON.stringify({ foo: 36 }); + + // Act + const result = SnsEnvelope.safeParse( + event, + JSONStringified( + z.object({ + foo: z.string(), + }) + ) + ); + + // Assess + expect(result).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; - - snsSqsTestEvent.Records[0].body = 'not a json string'; - - expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ - success: false, - error: expect.any(SyntaxError), - originalEvent: snsSqsTestEvent, - }); - }); - }); -}); -describe('SnsEnvelope', () => { - describe('parse', () => { - it('should parse custom schema in envelope', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; - - const testRecords = [] as z.infer[]; - - testEvent.Records.map((record) => { - const value = generateMock(TestSchema); - testRecords.push(value); - record.Sns.Message = JSON.stringify(value); - }); - - expect(SnsEnvelope.parse(testEvent, TestSchema)).toEqual(testRecords); - }); - - it('should throw if message does not macht schema', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; - - testEvent.Records.map((record) => { - record.Sns.Message = JSON.stringify({ - foo: 'bar', - }); + error: new ParseError('Failed to parse SNS message at index 1', { + cause: new ZodError([ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: ['Records', 1, 'Sns', 'Message', 'foo'], + message: 'Expected string, received number', + }, + ]), + }), + originalEvent: event, }); - - expect(() => SnsEnvelope.parse(testEvent, TestSchema)).toThrow(); - }); - it('should throw if envelope is not valid', () => { - expect(() => SnsEnvelope.parse({ foo: 'bar' }, TestSchema)).toThrow(); }); - }); - describe('safeParse', () => { - it('should parse custom schema in envelope', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; - - const testRecords = [] as z.infer[]; - testEvent.Records.map((record) => { - const value = generateMock(TestSchema); - testRecords.push(value); - record.Sns.Message = JSON.stringify(value); - }); - - expect(SnsEnvelope.safeParse(testEvent, TestSchema)).toEqual({ - success: true, - data: testRecords, - }); - }); - - it('should return error when message does not macht schema', () => { - const testEvent = TestEvents.snsEvent as SNSEvent; - - testEvent.Records.map((record) => { - record.Sns.Message = JSON.stringify({ - foo: 'bar', - }); - }); - - const parseResult = SnsEnvelope.safeParse(testEvent, TestSchema); - expect(parseResult).toEqual({ - success: false, - error: expect.any(ParseError), - originalEvent: testEvent, - }); - - if (!parseResult.success && parseResult.error) { - expect(parseResult.error.cause).toBeInstanceOf(ZodError); - } - }); - it('should return error when envelope is not valid', () => { - expect(SnsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + it('returns a combined error if multiple records fail to parse', () => { + // Prepare + const event = structuredClone(baseEvent); + event.Records[1] = structuredClone(event.Records[0]); + + // Act + const result = SnsEnvelope.safeParse( + event, + JSONStringified( + z.object({ + foo: z.string(), + }) + ) + ); + + // Assess + expect(result).toEqual({ success: false, - error: expect.any(ParseError), - originalEvent: { foo: 'bar' }, + error: new ParseError('Failed to parse SNS messages at indexes 0, 1', { + cause: new ZodError([ + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 0, 'Sns', 'Message'], + }, + { + code: 'custom', + message: 'Invalid JSON', + path: ['Records', 1, 'Sns', 'Message'], + }, + ]), + }), + originalEvent: event, }); }); }); diff --git a/packages/parser/tests/unit/envelopes/sqs.test.ts b/packages/parser/tests/unit/envelopes/sqs.test.ts index 9e09bbf1d2..004d1d71f9 100644 --- a/packages/parser/tests/unit/envelopes/sqs.test.ts +++ b/packages/parser/tests/unit/envelopes/sqs.test.ts @@ -2,7 +2,7 @@ import { generateMock } from '@anatine/zod-mock'; import type { SQSEvent } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; import { ZodError } from 'zod'; -import { SqsEnvelope } from '../../../src/envelopes/sqs.js'; +import { SnsSqsEnvelope, SqsEnvelope } from '../../../src/envelopes/sqs.js'; import { ParseError } from '../../../src/errors.js'; import { TestEvents, TestSchema } from '../schema/utils.js'; @@ -68,4 +68,85 @@ describe('SqsEnvelope ', () => { }); }); }); + + 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; + + 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.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: true, + data: [data], + }); + }); + it('should return error when envelope is not valid', () => { + expect(SnsSqsEnvelope.safeParse({ foo: 'bar' }, TestSchema)).toEqual({ + success: false, + error: expect.any(ParseError), + originalEvent: { foo: 'bar' }, + }); + }); + 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; + + snsSqsTestEvent.Records[0].body = 'not a json string'; + + expect(SnsSqsEnvelope.safeParse(snsSqsTestEvent, TestSchema)).toEqual({ + success: false, + error: expect.any(SyntaxError), + originalEvent: snsSqsTestEvent, + }); + }); + }); }); diff --git a/packages/parser/tests/unit/helpers.test.ts b/packages/parser/tests/unit/helpers.test.ts index ae25400366..f8c4ce0b0a 100644 --- a/packages/parser/tests/unit/helpers.test.ts +++ b/packages/parser/tests/unit/helpers.test.ts @@ -133,8 +133,8 @@ describe('JSONStringified', () => { it('should parse extended SnsSchema', () => { // Prepare const testEvent = getTestEvent({ - eventsPath: '.', - filename: 'snsEvent', + eventsPath: 'sns', + filename: 'base', }); testEvent.Records[0].Sns.Message = JSON.stringify(basePayload); diff --git a/packages/parser/tests/unit/schema/sns.test.ts b/packages/parser/tests/unit/schema/sns.test.ts index 8ea5bde8fc..808344e5fa 100644 --- a/packages/parser/tests/unit/schema/sns.test.ts +++ b/packages/parser/tests/unit/schema/sns.test.ts @@ -1,43 +1,66 @@ import { describe, expect, it } from 'vitest'; -import { - SnsNotificationSchema, - SnsRecordSchema, - SnsSchema, - SnsSqsNotificationSchema, -} from '../../../src/schemas/'; -import type { SnsEvent, SqsEvent } from '../../../src/types'; -import type { - SnsNotification, - SnsRecord, - SnsSqsNotification, -} from '../../../src/types/schema'; -import { TestEvents } from './utils.js'; +import { SnsSchema } from '../../../src/schemas/sns.js'; +import type { SnsEvent } from '../../../src/types/schema.js'; +import { getTestEvent } from './utils.js'; -describe('SNS', () => { - it('should parse sns event', () => { - const snsEvent = TestEvents.snsEvent; - expect(SnsSchema.parse(snsEvent)).toEqual(snsEvent); +describe('Schema: SNS', () => { + const baseEvent = getTestEvent({ + eventsPath: 'sns', + filename: 'base', }); - it('should parse record from sns event', () => { - const snsEvent: SnsEvent = TestEvents.snsEvent as SnsEvent; - const parsed: SnsRecord = SnsRecordSchema.parse(snsEvent.Records[0]); - expect(parsed.Sns.Message).toEqual('Hello from SNS!'); - }); - it('should parse sns notification from sns event', () => { - const snsEvent: SnsEvent = TestEvents.snsEvent as SnsEvent; - const parsed: SnsNotification = SnsNotificationSchema.parse( - snsEvent.Records[0].Sns - ); - expect(parsed.Message).toEqual('Hello from SNS!'); + + it('parses a SNS event', () => { + // Prepare + const event = structuredClone(baseEvent); + + // Act + const result = SnsSchema.parse(event); + + // Assess + expect(result).toStrictEqual({ + Records: [ + { + EventSource: 'aws:sns', + EventVersion: '1.0', + EventSubscriptionArn: + 'arn:aws:sns:us-east-2:123456789012:ExampleTopic', + Sns: { + SignatureVersion: '1', + Timestamp: '2019-01-02T12:45:07.000Z', + Signature: + 'tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==', + SigningCertUrl: + 'https://sns.us-east-2.amazonaws.com/SimpleNotification', + MessageId: '95df01b4-ee98-5cb9-9903-4c221d41eb5e', + Message: 'Hello from SNS!', + MessageAttributes: { + Test: { + Type: 'String', + Value: 'TestString', + }, + TestBinary: { + Type: 'Binary', + Value: 'TestBinary', + }, + }, + Type: 'Notification', + UnsubscribeUrl: + 'https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe', + TopicArn: 'arn:aws:sns:us-east-2:123456789012:ExampleTopic', + Subject: 'TestInvoke', + }, + }, + ], + }); }); - it('should parse sns notification from sqs -> sns event', () => { - const sqsEvent: SqsEvent = TestEvents.snsSqsEvent as SqsEvent; - console.log(sqsEvent.Records[0].body); - const parsed: SnsSqsNotification = SnsSqsNotificationSchema.parse( - JSON.parse(sqsEvent.Records[0].body) - ); - expect(parsed.TopicArn).toEqual( - 'arn:aws:sns:eu-west-1:231436140809:powertools265' - ); + + it('throws if the event is not a SNS event', () => { + // Prepare + const event = { + Records: [], + }; + + // Act & Assess + expect(() => SnsSchema.parse(event)).toThrow(); }); }); diff --git a/packages/parser/tests/unit/schema/utils.ts b/packages/parser/tests/unit/schema/utils.ts index fe2ec91565..1a836462de 100644 --- a/packages/parser/tests/unit/schema/utils.ts +++ b/packages/parser/tests/unit/schema/utils.ts @@ -62,7 +62,6 @@ const filenames = [ 's3SqsEvent', 'secretsManagerEvent', 'sesEvent', - 'snsEvent', 'snsSqsEvent', 'snsSqsFifoEvent', 'sqsEvent', From fe90b404770f5018313517b6e463685b733d2e8a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 21 Jan 2025 18:37:22 +0100 Subject: [PATCH 2/2] fix(parser): move export --- packages/parser/src/envelopes/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/parser/src/envelopes/index.ts b/packages/parser/src/envelopes/index.ts index 3d1487a7b0..6ab0522ecd 100644 --- a/packages/parser/src/envelopes/index.ts +++ b/packages/parser/src/envelopes/index.ts @@ -7,7 +7,7 @@ export { KafkaEnvelope } from './kafka.js'; export { KinesisEnvelope } from './kinesis.js'; export { KinesisFirehoseEnvelope } from './kinesis-firehose.js'; export { LambdaFunctionUrlEnvelope } from './lambda.js'; -export { SnsEnvelope, SnsSqsEnvelope } from './sns.js'; -export { SqsEnvelope } from './sqs.js'; +export { SnsEnvelope } from './sns.js'; +export { SqsEnvelope, SnsSqsEnvelope } from './sqs.js'; export { VpcLatticeEnvelope } from './vpc-lattice.js'; export { VpcLatticeV2Envelope } from './vpc-latticev2.js';