Skip to content

fix(parser): set min length of 1 to s3 event lists #3524

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 2 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
4 changes: 2 additions & 2 deletions packages/parser/src/schemas/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ const S3EventNotificationEventBridgeSchema = EventBridgeSchema.extend({
* @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html}
*/
const S3Schema = z.object({
Records: z.array(S3RecordSchema),
Records: z.array(S3RecordSchema).min(1),
});

const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({
Expand Down Expand Up @@ -204,7 +204,7 @@ const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({
* @see {@link types.S3SqsEventNotification | S3SqsEventNotification }
*/
const S3SqsEventNotificationSchema = z.object({
Records: z.array(S3SqsEventNotificationRecordSchema),
Records: z.array(S3SqsEventNotificationRecordSchema).min(1),
});

const S3ObjectContext = z.object({
Expand Down
254 changes: 184 additions & 70 deletions packages/parser/tests/unit/schema/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,98 +4,212 @@ import {
S3ObjectLambdaEventSchema,
S3Schema,
S3SqsEventNotificationSchema,
} from '../../../src/schemas/';
import { TestEvents } from './utils.js';
} from '../../../src/schemas/s3.js';
import type {
S3Event,
S3EventNotificationEventBridge,
S3ObjectLambdaEvent,
S3SqsEventNotification,
} from '../../../src/types/schema.js';
import { getTestEvent, omit } from './utils.js';

describe('S3 ', () => {
it('should parse s3 event', () => {
const s3Event = TestEvents.s3Event;
describe('Schema: S3', () => {
const eventsPath = 's3';
const baseEvent = getTestEvent<S3Event>({
eventsPath,
filename: 'base',
});
const baseLambdaEvent = getTestEvent<S3ObjectLambdaEvent>({
eventsPath,
filename: 'object-iam-user',
});

it('parses an S3 event', () => {
// Prepare
const event = structuredClone(baseEvent);

expect(S3Schema.parse(s3Event)).toEqual(s3Event);
// Act
const result = S3Schema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('should parse s3 event bridge notification event created', () => {
const s3EventBridgeNotificationObjectCreatedEvent =
TestEvents.s3EventBridgeNotificationObjectCreatedEvent;
it('throws if the event is not an S3 event', () => {
// Prepare
const event = {
Records: [],
};

expect(
S3EventNotificationEventBridgeSchema.parse(
s3EventBridgeNotificationObjectCreatedEvent
)
).toEqual(s3EventBridgeNotificationObjectCreatedEvent);
// Act & Assess
expect(() => S3Schema.parse(event)).toThrow();
});

it('should parse s3 event bridge notification event detelted', () => {
const s3EventBridgeNotificationObjectDeletedEvent =
TestEvents.s3EventBridgeNotificationObjectDeletedEvent;
it('throws if the event is missing required fields', () => {
// Prepare
const event = structuredClone(baseEvent);
// @ts-expect-error - Intentionally remove required field
event.Records[0].s3.bucket.name = undefined;

expect(
S3EventNotificationEventBridgeSchema.parse(
s3EventBridgeNotificationObjectDeletedEvent
)
).toEqual(s3EventBridgeNotificationObjectDeletedEvent);
// Act & Assess
expect(() => S3Schema.parse(event)).toThrow();
});
it('should parse s3 event bridge notification event expired', () => {
const s3EventBridgeNotificationObjectExpiredEvent =
TestEvents.s3EventBridgeNotificationObjectExpiredEvent;

expect(
S3EventNotificationEventBridgeSchema.parse(
s3EventBridgeNotificationObjectExpiredEvent
)
).toEqual(s3EventBridgeNotificationObjectExpiredEvent);
it('parses an S3 Glacier event', () => {
// Prepare
const event = getTestEvent<S3Event>({
eventsPath,
filename: 'glacier',
});

// Act
const result = S3Schema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('parses an S3 event with a decoded key', () => {
// Prepare
const event = getTestEvent<S3Event>({
eventsPath,
filename: 'decoded-key',
});

// Act
const result = S3Schema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('should parse s3 sqs notification event', () => {
const s3SqsEvent = TestEvents.s3SqsEvent;
expect(S3SqsEventNotificationSchema.parse(s3SqsEvent)).toEqual(s3SqsEvent);
it('parses an S3 event with a deleted object', () => {
// Prepare
const event = getTestEvent<S3Event>({
eventsPath,
filename: 'delete-object',
});

// Act
const result = S3Schema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('should parse s3 event with decoded key', () => {
const s3EventDecodedKey = TestEvents.s3EventDecodedKey;
expect(S3Schema.parse(s3EventDecodedKey)).toEqual(s3EventDecodedKey);
it('parses an S3 Object Lambda with an IAM user', () => {
// Prepare
const event = structuredClone(baseLambdaEvent);

// Act
const result = S3ObjectLambdaEventSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('should parse s3 event delete object', () => {
const s3EventDeleteObject = TestEvents.s3EventDeleteObject;
expect(S3Schema.parse(s3EventDeleteObject)).toEqual(s3EventDeleteObject);
it('throws if the S3 Object Lambda event is missing required fields', () => {
// Prepare
const event = omit(['getObjectContext'], structuredClone(baseLambdaEvent));

// Act & Assess
expect(() => S3ObjectLambdaEventSchema.parse(event)).toThrow();
});

it('should parse s3 event glacier', () => {
const s3EventGlacier = TestEvents.s3EventGlacier;
expect(S3Schema.parse(s3EventGlacier)).toEqual(s3EventGlacier);
it('parses an S3 Object Lambda with temporary credentials', () => {
// Prepare
const event = getTestEvent<S3ObjectLambdaEvent>({
eventsPath,
filename: 'object-temp-credentials',
});
const expected = structuredClone(event);
// @ts-expect-error - Modifying the expected result to account for type coercion
expected.userIdentity.sessionContext.attributes.mfaAuthenticated = false;

// Act
const result = S3ObjectLambdaEventSchema.parse(event);

// Assess
expect(result).toStrictEqual(expected);
});

it('should parse s3 object event iam user', () => {
const s3ObjectEventIAMUser = TestEvents.s3ObjectEventIAMUser;
expect(S3ObjectLambdaEventSchema.parse(s3ObjectEventIAMUser)).toEqual(
s3ObjectEventIAMUser
);
it('parses an S3 Object Notification EventBridge event', () => {
// Prepare
const event = getTestEvent<S3EventNotificationEventBridge>({
eventsPath,
filename: 'eventbridge-object-created',
});

// Act
const result = S3EventNotificationEventBridgeSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('parses an S3 Object Notification EventBridge event for an object deleted', () => {
// Prepare
const event = getTestEvent<S3EventNotificationEventBridge>({
eventsPath,
filename: 'eventbridge-object-deleted',
});

// Act
const result = S3EventNotificationEventBridgeSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('parses an S3 Object Notification EventBridge event for an object expired', () => {
// Prepare
const event = getTestEvent<S3EventNotificationEventBridge>({
eventsPath,
filename: 'eventbridge-object-expired',
});

// Act
const result = S3EventNotificationEventBridgeSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('parses an S3 Object Notification EventBridge event for an object restored', () => {
// Prepare
const event = getTestEvent<S3EventNotificationEventBridge>({
eventsPath,
filename: 'eventbridge-object-restored',
});

// Act
const result = S3EventNotificationEventBridgeSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('parses an S3 event notification SQS event', () => {
// Prepare
const event = getTestEvent<S3SqsEventNotification>({
eventsPath,
filename: 'sqs-event',
});

// Prepare
const result = S3SqsEventNotificationSchema.parse(event);

// Assess
expect(result).toStrictEqual(event);
});

it('should parse s3 object event temp credentials', () => {
// ignore any because we don't want typed json
const s3ObjectEventTempCredentials =
// biome-ignore lint/suspicious/noExplicitAny: no specific typing needed
TestEvents.s3ObjectEventTempCredentials as any;
const parsed = S3ObjectLambdaEventSchema.parse(
s3ObjectEventTempCredentials
);
it('throws if the S3 event notification SQS event is not valid', () => {
// Prepare
const event = {
Records: [],
};

expect(parsed.userRequest).toEqual(
s3ObjectEventTempCredentials.userRequest
);
expect(parsed.getObjectContext).toEqual(
s3ObjectEventTempCredentials.getObjectContext
);
expect(parsed.configuration).toEqual(
s3ObjectEventTempCredentials.configuration
);
expect(parsed.userRequest).toEqual(
s3ObjectEventTempCredentials.userRequest
);
expect(
parsed.userIdentity?.sessionContext?.attributes.mfaAuthenticated
).toEqual(false);
// Act & Assess
expect(() => S3SqsEventNotificationSchema.parse(event)).toThrow();
});
});
12 changes: 0 additions & 12 deletions packages/parser/tests/unit/schema/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,6 @@ const filenames = [
'lambdaFunctionUrlEvent',
'lambdaFunctionUrlEventPathTrailingSlash',
'lambdaFunctionUrlIAMEvent',
's3Event',
's3EventBridgeNotificationObjectCreatedEvent',
's3EventBridgeNotificationObjectDeletedEvent',
's3EventBridgeNotificationObjectExpiredEvent',
's3EventBridgeNotificationObjectRestoreCompletedEvent',
's3EventDecodedKey',
's3EventDeleteObject',
's3EventDeleteObjectWithoutEtagSize',
's3EventGlacier',
's3ObjectEventIAMUser',
's3ObjectEventTempCredentials',
's3SqsEvent',
'sesEvent',
'vpcLatticeEvent',
'vpcLatticeEventPathTrailingSlash',
Expand Down
Loading