Skip to content

Commit 937be64

Browse files
authored
fix(parser): set min length of 1 to s3 event lists (#3524)
1 parent 5bb4a6a commit 937be64

14 files changed

+186
-84
lines changed

Diff for: packages/parser/src/schemas/s3.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const S3EventNotificationEventBridgeSchema = EventBridgeSchema.extend({
166166
* @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html}
167167
*/
168168
const S3Schema = z.object({
169-
Records: z.array(S3RecordSchema),
169+
Records: z.array(S3RecordSchema).min(1),
170170
});
171171

172172
const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({
@@ -204,7 +204,7 @@ const S3SqsEventNotificationRecordSchema = SqsRecordSchema.extend({
204204
* @see {@link types.S3SqsEventNotification | S3SqsEventNotification }
205205
*/
206206
const S3SqsEventNotificationSchema = z.object({
207-
Records: z.array(S3SqsEventNotificationRecordSchema),
207+
Records: z.array(S3SqsEventNotificationRecordSchema).min(1),
208208
});
209209

210210
const S3ObjectContext = z.object({

Diff for: packages/parser/tests/unit/schema/s3.test.ts

+184-70
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,212 @@ import {
44
S3ObjectLambdaEventSchema,
55
S3Schema,
66
S3SqsEventNotificationSchema,
7-
} from '../../../src/schemas/';
8-
import { TestEvents } from './utils.js';
7+
} from '../../../src/schemas/s3.js';
8+
import type {
9+
S3Event,
10+
S3EventNotificationEventBridge,
11+
S3ObjectLambdaEvent,
12+
S3SqsEventNotification,
13+
} from '../../../src/types/schema.js';
14+
import { getTestEvent, omit } from './utils.js';
915

10-
describe('S3 ', () => {
11-
it('should parse s3 event', () => {
12-
const s3Event = TestEvents.s3Event;
16+
describe('Schema: S3', () => {
17+
const eventsPath = 's3';
18+
const baseEvent = getTestEvent<S3Event>({
19+
eventsPath,
20+
filename: 'base',
21+
});
22+
const baseLambdaEvent = getTestEvent<S3ObjectLambdaEvent>({
23+
eventsPath,
24+
filename: 'object-iam-user',
25+
});
26+
27+
it('parses an S3 event', () => {
28+
// Prepare
29+
const event = structuredClone(baseEvent);
1330

14-
expect(S3Schema.parse(s3Event)).toEqual(s3Event);
31+
// Act
32+
const result = S3Schema.parse(event);
33+
34+
// Assess
35+
expect(result).toStrictEqual(event);
1536
});
1637

17-
it('should parse s3 event bridge notification event created', () => {
18-
const s3EventBridgeNotificationObjectCreatedEvent =
19-
TestEvents.s3EventBridgeNotificationObjectCreatedEvent;
38+
it('throws if the event is not an S3 event', () => {
39+
// Prepare
40+
const event = {
41+
Records: [],
42+
};
2043

21-
expect(
22-
S3EventNotificationEventBridgeSchema.parse(
23-
s3EventBridgeNotificationObjectCreatedEvent
24-
)
25-
).toEqual(s3EventBridgeNotificationObjectCreatedEvent);
44+
// Act & Assess
45+
expect(() => S3Schema.parse(event)).toThrow();
2646
});
2747

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

32-
expect(
33-
S3EventNotificationEventBridgeSchema.parse(
34-
s3EventBridgeNotificationObjectDeletedEvent
35-
)
36-
).toEqual(s3EventBridgeNotificationObjectDeletedEvent);
54+
// Act & Assess
55+
expect(() => S3Schema.parse(event)).toThrow();
3756
});
38-
it('should parse s3 event bridge notification event expired', () => {
39-
const s3EventBridgeNotificationObjectExpiredEvent =
40-
TestEvents.s3EventBridgeNotificationObjectExpiredEvent;
4157

42-
expect(
43-
S3EventNotificationEventBridgeSchema.parse(
44-
s3EventBridgeNotificationObjectExpiredEvent
45-
)
46-
).toEqual(s3EventBridgeNotificationObjectExpiredEvent);
58+
it('parses an S3 Glacier event', () => {
59+
// Prepare
60+
const event = getTestEvent<S3Event>({
61+
eventsPath,
62+
filename: 'glacier',
63+
});
64+
65+
// Act
66+
const result = S3Schema.parse(event);
67+
68+
// Assess
69+
expect(result).toStrictEqual(event);
70+
});
71+
72+
it('parses an S3 event with a decoded key', () => {
73+
// Prepare
74+
const event = getTestEvent<S3Event>({
75+
eventsPath,
76+
filename: 'decoded-key',
77+
});
78+
79+
// Act
80+
const result = S3Schema.parse(event);
81+
82+
// Assess
83+
expect(result).toStrictEqual(event);
4784
});
4885

49-
it('should parse s3 sqs notification event', () => {
50-
const s3SqsEvent = TestEvents.s3SqsEvent;
51-
expect(S3SqsEventNotificationSchema.parse(s3SqsEvent)).toEqual(s3SqsEvent);
86+
it('parses an S3 event with a deleted object', () => {
87+
// Prepare
88+
const event = getTestEvent<S3Event>({
89+
eventsPath,
90+
filename: 'delete-object',
91+
});
92+
93+
// Act
94+
const result = S3Schema.parse(event);
95+
96+
// Assess
97+
expect(result).toStrictEqual(event);
5298
});
5399

54-
it('should parse s3 event with decoded key', () => {
55-
const s3EventDecodedKey = TestEvents.s3EventDecodedKey;
56-
expect(S3Schema.parse(s3EventDecodedKey)).toEqual(s3EventDecodedKey);
100+
it('parses an S3 Object Lambda with an IAM user', () => {
101+
// Prepare
102+
const event = structuredClone(baseLambdaEvent);
103+
104+
// Act
105+
const result = S3ObjectLambdaEventSchema.parse(event);
106+
107+
// Assess
108+
expect(result).toStrictEqual(event);
57109
});
58110

59-
it('should parse s3 event delete object', () => {
60-
const s3EventDeleteObject = TestEvents.s3EventDeleteObject;
61-
expect(S3Schema.parse(s3EventDeleteObject)).toEqual(s3EventDeleteObject);
111+
it('throws if the S3 Object Lambda event is missing required fields', () => {
112+
// Prepare
113+
const event = omit(['getObjectContext'], structuredClone(baseLambdaEvent));
114+
115+
// Act & Assess
116+
expect(() => S3ObjectLambdaEventSchema.parse(event)).toThrow();
62117
});
63118

64-
it('should parse s3 event glacier', () => {
65-
const s3EventGlacier = TestEvents.s3EventGlacier;
66-
expect(S3Schema.parse(s3EventGlacier)).toEqual(s3EventGlacier);
119+
it('parses an S3 Object Lambda with temporary credentials', () => {
120+
// Prepare
121+
const event = getTestEvent<S3ObjectLambdaEvent>({
122+
eventsPath,
123+
filename: 'object-temp-credentials',
124+
});
125+
const expected = structuredClone(event);
126+
// @ts-expect-error - Modifying the expected result to account for type coercion
127+
expected.userIdentity.sessionContext.attributes.mfaAuthenticated = false;
128+
129+
// Act
130+
const result = S3ObjectLambdaEventSchema.parse(event);
131+
132+
// Assess
133+
expect(result).toStrictEqual(expected);
67134
});
68135

69-
it('should parse s3 object event iam user', () => {
70-
const s3ObjectEventIAMUser = TestEvents.s3ObjectEventIAMUser;
71-
expect(S3ObjectLambdaEventSchema.parse(s3ObjectEventIAMUser)).toEqual(
72-
s3ObjectEventIAMUser
73-
);
136+
it('parses an S3 Object Notification EventBridge event', () => {
137+
// Prepare
138+
const event = getTestEvent<S3EventNotificationEventBridge>({
139+
eventsPath,
140+
filename: 'eventbridge-object-created',
141+
});
142+
143+
// Act
144+
const result = S3EventNotificationEventBridgeSchema.parse(event);
145+
146+
// Assess
147+
expect(result).toStrictEqual(event);
148+
});
149+
150+
it('parses an S3 Object Notification EventBridge event for an object deleted', () => {
151+
// Prepare
152+
const event = getTestEvent<S3EventNotificationEventBridge>({
153+
eventsPath,
154+
filename: 'eventbridge-object-deleted',
155+
});
156+
157+
// Act
158+
const result = S3EventNotificationEventBridgeSchema.parse(event);
159+
160+
// Assess
161+
expect(result).toStrictEqual(event);
162+
});
163+
164+
it('parses an S3 Object Notification EventBridge event for an object expired', () => {
165+
// Prepare
166+
const event = getTestEvent<S3EventNotificationEventBridge>({
167+
eventsPath,
168+
filename: 'eventbridge-object-expired',
169+
});
170+
171+
// Act
172+
const result = S3EventNotificationEventBridgeSchema.parse(event);
173+
174+
// Assess
175+
expect(result).toStrictEqual(event);
176+
});
177+
178+
it('parses an S3 Object Notification EventBridge event for an object restored', () => {
179+
// Prepare
180+
const event = getTestEvent<S3EventNotificationEventBridge>({
181+
eventsPath,
182+
filename: 'eventbridge-object-restored',
183+
});
184+
185+
// Act
186+
const result = S3EventNotificationEventBridgeSchema.parse(event);
187+
188+
// Assess
189+
expect(result).toStrictEqual(event);
190+
});
191+
192+
it('parses an S3 event notification SQS event', () => {
193+
// Prepare
194+
const event = getTestEvent<S3SqsEventNotification>({
195+
eventsPath,
196+
filename: 'sqs-event',
197+
});
198+
199+
// Prepare
200+
const result = S3SqsEventNotificationSchema.parse(event);
201+
202+
// Assess
203+
expect(result).toStrictEqual(event);
74204
});
75205

76-
it('should parse s3 object event temp credentials', () => {
77-
// ignore any because we don't want typed json
78-
const s3ObjectEventTempCredentials =
79-
// biome-ignore lint/suspicious/noExplicitAny: no specific typing needed
80-
TestEvents.s3ObjectEventTempCredentials as any;
81-
const parsed = S3ObjectLambdaEventSchema.parse(
82-
s3ObjectEventTempCredentials
83-
);
206+
it('throws if the S3 event notification SQS event is not valid', () => {
207+
// Prepare
208+
const event = {
209+
Records: [],
210+
};
84211

85-
expect(parsed.userRequest).toEqual(
86-
s3ObjectEventTempCredentials.userRequest
87-
);
88-
expect(parsed.getObjectContext).toEqual(
89-
s3ObjectEventTempCredentials.getObjectContext
90-
);
91-
expect(parsed.configuration).toEqual(
92-
s3ObjectEventTempCredentials.configuration
93-
);
94-
expect(parsed.userRequest).toEqual(
95-
s3ObjectEventTempCredentials.userRequest
96-
);
97-
expect(
98-
parsed.userIdentity?.sessionContext?.attributes.mfaAuthenticated
99-
).toEqual(false);
212+
// Act & Assess
213+
expect(() => S3SqsEventNotificationSchema.parse(event)).toThrow();
100214
});
101215
});

Diff for: packages/parser/tests/unit/schema/utils.ts

-12
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,6 @@ const filenames = [
2323
'lambdaFunctionUrlEvent',
2424
'lambdaFunctionUrlEventPathTrailingSlash',
2525
'lambdaFunctionUrlIAMEvent',
26-
's3Event',
27-
's3EventBridgeNotificationObjectCreatedEvent',
28-
's3EventBridgeNotificationObjectDeletedEvent',
29-
's3EventBridgeNotificationObjectExpiredEvent',
30-
's3EventBridgeNotificationObjectRestoreCompletedEvent',
31-
's3EventDecodedKey',
32-
's3EventDeleteObject',
33-
's3EventDeleteObjectWithoutEtagSize',
34-
's3EventGlacier',
35-
's3ObjectEventIAMUser',
36-
's3ObjectEventTempCredentials',
37-
's3SqsEvent',
3826
'sesEvent',
3927
'vpcLatticeEvent',
4028
'vpcLatticeEventPathTrailingSlash',

0 commit comments

Comments
 (0)