Skip to content

fix(parser): min array length on Records #3521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 };
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
Loading