Skip to content

Commit

Permalink
fix(parser): min array length on Records (#3521)
Browse files Browse the repository at this point in the history
  • Loading branch information
dreamorosi authored Jan 24, 2025
1 parent 937be64 commit 89a6281
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 250 deletions.
2 changes: 1 addition & 1 deletion packages/parser/src/schemas/ses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const SesRecordSchema = z.object({
* @see {@link https://docs.aws.amazon.com/ses/latest/dg/receiving-email-notifications-examples.html}
*/
const SesSchema = z.object({
Records: z.array(SesRecordSchema),
Records: z.array(SesRecordSchema).min(1),
});

export { SesSchema, SesRecordSchema };
File renamed without changes.
3 changes: 2 additions & 1 deletion packages/parser/tests/unit/envelopes/sqs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ describe('Envelope: SqsEnvelope', () => {
expect(result).toStrictEqual([{ message: 'hello' }, { message: 'foo1' }]);
});
});
describe('safeParse', () => {

describe('Method: safeParse', () => {
it('parses an SQS event', () => {
// Prepare
const event = structuredClone(baseEvent);
Expand Down
180 changes: 65 additions & 115 deletions packages/parser/tests/unit/parser.decorator.test.ts
Original file line number Diff line number Diff line change
@@ -1,159 +1,109 @@
import { generateMock } from '@anatine/zod-mock';
import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types';
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
import type { Context } from 'aws-lambda';
import { describe, expect, it } from 'vitest';
import type { z } from 'zod';
import { type ZodSchema, z } from 'zod';
import { EventBridgeEnvelope } from '../../src/envelopes/index.js';
import { ParseError } from '../../src/errors.js';
import { parser } from '../../src/index.js';
import { EventBridgeSchema } from '../../src/schemas/index.js';
import type { EventBridgeEvent, ParsedResult } from '../../src/types';
import { TestSchema, getTestEvent } from './schema/utils';
import type { EventBridgeEvent, ParsedResult } from '../../src/types/index.js';
import { getTestEvent } from './schema/utils.js';

describe('Parser Decorator', () => {
const customEventBridgeSchema = EventBridgeSchema.extend({
detail: TestSchema,
describe('Decorator: parser', () => {
const schema = z.object({
name: z.string(),
age: z.number(),
});
const payload = {
name: 'John Doe',
age: 30,
};
const extendedSchema = EventBridgeSchema.extend({
detail: schema,
});
type event = z.infer<typeof extendedSchema>;
const baseEvent = getTestEvent<EventBridgeEvent>({
eventsPath: 'eventbridge',
filename: 'base',
});

type TestEvent = z.infer<typeof TestSchema>;

class TestClass implements LambdaInterface {
@parser({ schema: TestSchema })
public async handler(
event: TestEvent,
_context: Context
): Promise<TestEvent> {
return event;
}

@parser({ schema: customEventBridgeSchema })
public async handlerWithCustomSchema(
event: unknown,
_context: Context
): Promise<unknown> {
@parser({ schema: extendedSchema })
public async handler(event: event, _context: Context): Promise<event> {
return event;
}

@parser({ schema: TestSchema, envelope: EventBridgeEnvelope })
@parser({ schema, envelope: EventBridgeEnvelope })
public async handlerWithParserCallsAnotherMethod(
event: TestEvent,
event: z.infer<typeof schema>,
_context: Context
): Promise<unknown> {
return this.anotherMethod(event);
}

@parser({ schema: TestSchema, envelope: EventBridgeEnvelope })
public async handlerWithSchemaAndEnvelope(
event: TestEvent,
_context: Context
): Promise<unknown> {
return event;
}

@parser({
schema: TestSchema,
schema,
safeParse: true,
})
public async handlerWithSchemaAndSafeParse(
event: ParsedResult<unknown, TestEvent>,
event: ParsedResult<unknown, event>,
_context: Context
): Promise<ParsedResult> {
): Promise<ParsedResult<unknown, event>> {
return event;
}

@parser({
schema: TestSchema,
schema,
envelope: EventBridgeEnvelope,
safeParse: true,
})
public async harndlerWithEnvelopeAndSafeParse(
event: ParsedResult<TestEvent, TestEvent>,
event: ParsedResult<event, event>,
_context: Context
): Promise<ParsedResult> {
return event;
}

private async anotherMethod(event: TestEvent): Promise<TestEvent> {
private async anotherMethod<T extends ZodSchema>(
event: z.infer<T>
): Promise<z.infer<T>> {
return event;
}
}

const lambda = new TestClass();

it('should parse custom schema event', async () => {
const testEvent = generateMock(TestSchema);
it('parses the event using the schema provided', async () => {
// Prepare
const event = structuredClone(baseEvent);
event.detail = payload;

const resp = await lambda.handler(testEvent, {} as Context);
// Act
// @ts-expect-error - extended schema
const result = await lambda.handler(event, {} as Context);

expect(resp).toEqual(testEvent);
// Assess
expect(result).toEqual(event);
});

it('should parse custom schema with envelope event', async () => {
const customPayload = generateMock(TestSchema);
const testEvent = getTestEvent<EventBridgeEvent>({
eventsPath: 'eventbridge',
filename: 'base',
});
testEvent.detail = customPayload;
it('preserves the class method scope when decorated', async () => {
// Prepare
const event = structuredClone(baseEvent);
event.detail = payload;

const resp = await lambda.handlerWithSchemaAndEnvelope(
testEvent as unknown as TestEvent,
const result = await lambda.handlerWithParserCallsAnotherMethod(
// @ts-expect-error - extended schema
event,
{} as Context
);

expect(resp).toEqual(customPayload);
expect(result).toEqual(event.detail);
});

it('should parse extended envelope event', async () => {
const customPayload = generateMock(TestSchema);

const testEvent = generateMock(customEventBridgeSchema);
testEvent.detail = customPayload;

const resp: z.infer<typeof customEventBridgeSchema> =
(await lambda.handlerWithCustomSchema(
testEvent,
{} as Context
)) as z.infer<typeof customEventBridgeSchema>;

expect(customEventBridgeSchema.parse(resp)).toEqual(testEvent);
expect(resp.detail).toEqual(customPayload);
});

it('should parse and call private async method', async () => {
const customPayload = generateMock(TestSchema);
const testEvent = getTestEvent<EventBridgeEvent>({
eventsPath: 'eventbridge',
filename: 'base',
});
testEvent.detail = customPayload;

const resp = await lambda.handlerWithParserCallsAnotherMethod(
testEvent as unknown as TestEvent,
{} as Context
);

expect(resp).toEqual(customPayload);
});

it('should parse event with schema and safeParse', async () => {
const testEvent = generateMock(TestSchema);

const resp = await lambda.handlerWithSchemaAndSafeParse(
testEvent as unknown as ParsedResult<unknown, TestEvent>,
{} as Context
);

expect(resp).toEqual({
success: true,
data: testEvent,
});
});

it('should parse event with schema and safeParse and return error', async () => {
it('returns a parse error when schema validation fails with safeParse enabled', async () => {
// Act & Assess
expect(
await lambda.handlerWithSchemaAndSafeParse(
{ foo: 'bar' } as unknown as ParsedResult<unknown, TestEvent>,
{ foo: 'bar' } as unknown as ParsedResult<unknown, event>,
{} as Context
)
).toEqual({
Expand All @@ -163,29 +113,29 @@ describe('Parser Decorator', () => {
});
});

it('should parse event with envelope and safeParse', async () => {
const testEvent = generateMock(TestSchema);
const event = getTestEvent<EventBridgeEvent>({
eventsPath: 'eventbridge',
filename: 'base',
});
event.detail = testEvent;
it('parses the event with envelope and safeParse', async () => {
// Prepare
const event = structuredClone(baseEvent);
event.detail = payload;

const resp = await lambda.harndlerWithEnvelopeAndSafeParse(
event as unknown as ParsedResult<TestEvent, TestEvent>,
// Act
const result = await lambda.harndlerWithEnvelopeAndSafeParse(
event as unknown as ParsedResult<event, event>,
{} as Context
);

expect(resp).toEqual({
// Assess
expect(result).toEqual({
success: true,
data: testEvent,
data: event.detail,
});
});

it('should parse event with envelope and safeParse and return error', async () => {
it('returns a parse error when schema/envelope validation fails with safeParse enabled', async () => {
// Act & Assess
expect(
await lambda.harndlerWithEnvelopeAndSafeParse(
{ foo: 'bar' } as unknown as ParsedResult<TestEvent, TestEvent>,
{ foo: 'bar' } as unknown as ParsedResult<event, event>,
{} as Context
)
).toEqual({
Expand Down
99 changes: 27 additions & 72 deletions packages/parser/tests/unit/schema/appsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
/**
* Test built-in AppSync resolver schemas
*/

import { describe, expect, it } from 'vitest';
import {
AppSyncBatchResolverSchema,
AppSyncResolverSchema,
} from '../../../src/schemas/appsync';
import type { AppSyncResolverEvent } from '../../../src/types';
import { getTestEvent, omit } from './utils';
} from '../../../src/schemas/appsync.js';
import type { AppSyncResolverEvent } from '../../../src/types/schema.js';
import { getTestEvent, omit } from './utils.js';

describe('AppSync Resolver Schemas', () => {
describe('Schema: AppSync Resolver', () => {
const eventsPath = 'appsync';

const appSyncResolverEvent: AppSyncResolverEvent = getTestEvent({
const appSyncResolverEvent = getTestEvent<AppSyncResolverEvent>({
eventsPath,
filename: 'resolver',
});

const table = [
const events = [
{
name: 'null source',
event: {
Expand Down Expand Up @@ -119,73 +114,33 @@ describe('AppSync Resolver Schemas', () => {
},
];

describe('AppSync Resolver Schema', () => {
it('should return validation error when the event is invalid', () => {
const { error } = AppSyncResolverSchema.safeParse(
omit(['request', 'info'], appSyncResolverEvent)
);
it.each(events)('parses an AppSyn resolver event with $name', ({ event }) => {
// Assess
const result = AppSyncResolverSchema.parse(event);

expect(error?.issues).toEqual([
{
code: 'invalid_type',
expected: 'object',
received: 'undefined',
path: ['request'],
message: 'Required',
},
{
code: 'invalid_type',
expected: 'object',
received: 'undefined',
path: ['info'],
message: 'Required',
},
]);
});
// Assess
expect(result).toEqual(event);
});

it('should parse resolver event without identity field', () => {
const event: Omit<AppSyncResolverEvent, 'identity'> = omit(
['identity'],
appSyncResolverEvent
);
const parsedEvent = AppSyncResolverSchema.parse(event);
expect(parsedEvent).toEqual(event);
});
it('throws when the event is not an AppSync resolver event', () => {
// Prepare
const event = omit(
['request', 'info'],
structuredClone(appSyncResolverEvent)
);

it.each(table)('should parse resolver event with $name', ({ event }) => {
const parsedEvent = AppSyncResolverSchema.parse(event);
expect(parsedEvent).toEqual(event);
});
// Act & Assess
expect(() => AppSyncResolverSchema.parse(event)).toThrow();
});

describe('Batch AppSync Resolver Schema', () => {
it('should return validation error when the event is invalid', () => {
const event = omit(['request', 'info'], appSyncResolverEvent);

const { error } = AppSyncBatchResolverSchema.safeParse([event]);
it('parses batches of AppSync resolver events', () => {
// Prepare
const event = events.map((event) => structuredClone(event.event));

expect(error?.issues).toEqual([
{
code: 'invalid_type',
expected: 'object',
received: 'undefined',
path: [0, 'request'],
message: 'Required',
},
{
code: 'invalid_type',
expected: 'object',
received: 'undefined',
path: [0, 'info'],
message: 'Required',
},
]);
});
// Act
const result = AppSyncBatchResolverSchema.parse(event);

it('should parse batches of appsync resolver events', () => {
const events = table.map((table) => table.event);
const parsedEvent = AppSyncBatchResolverSchema.parse(events);
expect(parsedEvent).toEqual(events);
});
// Assess
expect(result).toEqual(event);
});
});
Loading

0 comments on commit 89a6281

Please sign in to comment.