lambder 1.0.147 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +355 -410
- package/dist/Lambder.d.ts +47 -21
- package/dist/Lambder.js +79 -24
- package/dist/LambderApiContract.d.ts +10 -42
- package/dist/LambderApiContract.js +2 -22
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderMSW.js +0 -4
- package/dist/LambderResolver.d.ts +16 -17
- package/dist/LambderResponseBuilder.d.ts +2 -3
- package/dist/LambderResponseBuilder.js +1 -3
- package/dist/LambderUtils.js +1 -3
- package/dist/index.d.ts +1 -1
- package/docs/LAMBDER_MSW.md +6 -6
- package/docs/TYPE_SAFE_QUICK_START.md +54 -177
- package/examples/msw-testing-example.ts +36 -33
- package/examples/secure-session-example.ts +50 -34
- package/examples/zod-chained-api-example.ts +63 -0
- package/package.json +3 -2
- package/src/Lambder.ts +124 -83
- package/src/LambderApiContract.ts +7 -50
- package/src/LambderCaller.ts +2 -2
- package/src/LambderMSW.ts +0 -7
- package/src/LambderResolver.ts +21 -24
- package/src/LambderResponseBuilder.ts +4 -7
- package/src/LambderUtils.ts +1 -3
- package/src/index.ts +0 -3
- package/tests/UNTESTED_FEATURES.md +263 -0
- package/tests/error-handling.test.ts +585 -0
- package/tests/hooks.test.ts +561 -0
- package/tests/output-type-runtime.test.ts +80 -64
- package/tests/routes.test.ts +542 -0
- package/tests/session.test.ts +38 -24
- package/tests/type-safety.test.ts +147 -97
- package/tests/use-plugin.test.ts +437 -0
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
- package/examples/output-type-enforcement-example.ts +0 -218
- package/examples/simplified-typed-api-example.ts +0 -365
- package/examples/test-output-type-enforcement.ts +0 -101
- package/test-type-enforcement.ts +0 -111
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for error handling including:
|
|
5
|
+
* - setGlobalErrorHandler functionality
|
|
6
|
+
* - Error handling with context available
|
|
7
|
+
* - Error handling with null context
|
|
8
|
+
* - Custom error responses
|
|
9
|
+
* - Error in API handlers
|
|
10
|
+
* - Error in route handlers
|
|
11
|
+
* - Error in hooks
|
|
12
|
+
* - Error log accumulation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import Lambder from '../src/Lambder.js';
|
|
18
|
+
import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
|
|
19
|
+
|
|
20
|
+
const createMockEvent = (path: string, method: string = 'GET', apiName?: string, payload?: any): APIGatewayProxyEvent => ({
|
|
21
|
+
body: apiName ? JSON.stringify({ apiName, payload }) : null,
|
|
22
|
+
headers: { Host: 'localhost' },
|
|
23
|
+
multiValueHeaders: {},
|
|
24
|
+
httpMethod: method,
|
|
25
|
+
isBase64Encoded: false,
|
|
26
|
+
path: apiName ? '/api' : path,
|
|
27
|
+
pathParameters: null,
|
|
28
|
+
queryStringParameters: null,
|
|
29
|
+
multiValueQueryStringParameters: null,
|
|
30
|
+
stageVariables: null,
|
|
31
|
+
requestContext: {} as any,
|
|
32
|
+
resource: '',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const createMockContext = (): Context => ({
|
|
36
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
37
|
+
functionName: 'test',
|
|
38
|
+
functionVersion: '1',
|
|
39
|
+
invokedFunctionArn: 'arn',
|
|
40
|
+
memoryLimitInMB: '128',
|
|
41
|
+
awsRequestId: '123',
|
|
42
|
+
logGroupName: 'group',
|
|
43
|
+
logStreamName: 'stream',
|
|
44
|
+
getRemainingTimeInMillis: () => 1000,
|
|
45
|
+
done: () => {},
|
|
46
|
+
fail: () => {},
|
|
47
|
+
succeed: () => {},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Error Handling - Global Error Handler', () => {
|
|
51
|
+
it('should catch errors in route handlers', async () => {
|
|
52
|
+
const lambder = new Lambder({
|
|
53
|
+
publicPath: './public',
|
|
54
|
+
apiPath: '/api'
|
|
55
|
+
})
|
|
56
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
57
|
+
return res.raw({
|
|
58
|
+
statusCode: 500,
|
|
59
|
+
body: `Error: ${err.message}`
|
|
60
|
+
});
|
|
61
|
+
})
|
|
62
|
+
.addRoute('/error', (ctx, res) => {
|
|
63
|
+
throw new Error('Route handler error');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const handler = lambder.getHandler();
|
|
67
|
+
const result = await handler(createMockEvent('/error'), createMockContext());
|
|
68
|
+
|
|
69
|
+
expect(result.statusCode).toBe(500);
|
|
70
|
+
expect(result.body).toBe('Error: Route handler error');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should catch errors in API handlers', async () => {
|
|
74
|
+
const lambder = new Lambder({
|
|
75
|
+
publicPath: './public',
|
|
76
|
+
apiPath: '/api'
|
|
77
|
+
})
|
|
78
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
79
|
+
if (ctx?._otherInternal.isApiCall) {
|
|
80
|
+
return res.api({ error: err.message });
|
|
81
|
+
}
|
|
82
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
83
|
+
})
|
|
84
|
+
.addApi('errorApi', {
|
|
85
|
+
input: z.object({ value: z.string() }),
|
|
86
|
+
output: z.object({ result: z.string() })
|
|
87
|
+
}, async (ctx, res) => {
|
|
88
|
+
throw new Error('API handler error');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const handler = lambder.getHandler();
|
|
92
|
+
const event = createMockEvent('/api', 'POST', 'errorApi', { value: 'test' });
|
|
93
|
+
const result = await handler(event, createMockContext());
|
|
94
|
+
|
|
95
|
+
expect(result.statusCode).toBe(200);
|
|
96
|
+
const body = JSON.parse(result.body || '{}');
|
|
97
|
+
expect(body.payload.error).toBe('API handler error');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should catch async errors', async () => {
|
|
101
|
+
const lambder = new Lambder({
|
|
102
|
+
publicPath: './public',
|
|
103
|
+
apiPath: '/api'
|
|
104
|
+
})
|
|
105
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
106
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
107
|
+
})
|
|
108
|
+
.addRoute('/async-error', async (ctx, res) => {
|
|
109
|
+
await Promise.resolve();
|
|
110
|
+
throw new Error('Async error');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const handler = lambder.getHandler();
|
|
114
|
+
const result = await handler(createMockEvent('/async-error'), createMockContext());
|
|
115
|
+
|
|
116
|
+
expect(result.statusCode).toBe(500);
|
|
117
|
+
expect(result.body).toBe('Async error');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should provide context in error handler', async () => {
|
|
121
|
+
let capturedContext: any = null;
|
|
122
|
+
|
|
123
|
+
const lambder = new Lambder({
|
|
124
|
+
publicPath: './public',
|
|
125
|
+
apiPath: '/api'
|
|
126
|
+
})
|
|
127
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
128
|
+
capturedContext = ctx;
|
|
129
|
+
return res.raw({ statusCode: 500, body: 'Error' });
|
|
130
|
+
})
|
|
131
|
+
.addRoute('/test', (ctx, res) => {
|
|
132
|
+
throw new Error('Test error');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const handler = lambder.getHandler();
|
|
136
|
+
await handler(createMockEvent('/test'), createMockContext());
|
|
137
|
+
|
|
138
|
+
expect(capturedContext).not.toBeNull();
|
|
139
|
+
expect(capturedContext.path).toBe('/test');
|
|
140
|
+
expect(capturedContext.method).toBe('GET');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle null context in error handler', async () => {
|
|
144
|
+
let capturedContext: any = undefined;
|
|
145
|
+
|
|
146
|
+
const lambder = new Lambder({
|
|
147
|
+
publicPath: './public',
|
|
148
|
+
apiPath: '/api'
|
|
149
|
+
})
|
|
150
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
151
|
+
capturedContext = ctx;
|
|
152
|
+
return res.raw({ statusCode: 500, body: 'Error occurred' });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const handler = lambder.getHandler();
|
|
156
|
+
|
|
157
|
+
// Create a malformed event that might cause early errors
|
|
158
|
+
const malformedEvent = {
|
|
159
|
+
...createMockEvent('/test'),
|
|
160
|
+
headers: null as any
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await handler(malformedEvent, createMockContext());
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// Expected to potentially fail
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Context might be null for early errors
|
|
170
|
+
expect(capturedContext === null || capturedContext !== undefined).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('Error Handling - Custom Error Responses', () => {
|
|
175
|
+
it('should return custom error format for APIs', async () => {
|
|
176
|
+
const lambder = new Lambder({
|
|
177
|
+
publicPath: './public',
|
|
178
|
+
apiPath: '/api'
|
|
179
|
+
})
|
|
180
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
181
|
+
if (ctx?._otherInternal.isApiCall) {
|
|
182
|
+
return res.api(
|
|
183
|
+
{ success: false, error: err.message },
|
|
184
|
+
{ errorMessage: err.message }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
188
|
+
})
|
|
189
|
+
.addApi('testApi', {
|
|
190
|
+
input: z.void(),
|
|
191
|
+
output: z.object({ success: z.boolean(), error: z.string().optional() })
|
|
192
|
+
}, async (ctx, res) => {
|
|
193
|
+
throw new Error('Custom error message');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const handler = lambder.getHandler();
|
|
197
|
+
const event = createMockEvent('/api', 'POST', 'testApi', undefined);
|
|
198
|
+
const result = await handler(event, createMockContext());
|
|
199
|
+
|
|
200
|
+
const body = JSON.parse(result.body || '{}');
|
|
201
|
+
expect(body.payload.success).toBe(false);
|
|
202
|
+
expect(body.payload.error).toBe('Custom error message');
|
|
203
|
+
expect(body.errorMessage).toBe('Custom error message');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should return HTML error for routes', async () => {
|
|
207
|
+
const lambder = new Lambder({
|
|
208
|
+
publicPath: './public',
|
|
209
|
+
apiPath: '/api'
|
|
210
|
+
})
|
|
211
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
212
|
+
return res.html(`<h1>Error</h1><p>${err.message}</p>`);
|
|
213
|
+
})
|
|
214
|
+
.addRoute('/page', (ctx, res) => {
|
|
215
|
+
throw new Error('Page load failed');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const handler = lambder.getHandler();
|
|
219
|
+
const result = await handler(createMockEvent('/page'), createMockContext());
|
|
220
|
+
|
|
221
|
+
expect(result.statusCode).toBe(200);
|
|
222
|
+
const body = Buffer.from(result.body || '', 'base64').toString();
|
|
223
|
+
expect(body).toContain('<h1>Error</h1>');
|
|
224
|
+
expect(body).toContain('Page load failed');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return JSON error for routes if desired', async () => {
|
|
228
|
+
const lambder = new Lambder({
|
|
229
|
+
publicPath: './public',
|
|
230
|
+
apiPath: '/api'
|
|
231
|
+
})
|
|
232
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
233
|
+
return res.json({
|
|
234
|
+
error: true,
|
|
235
|
+
message: err.message,
|
|
236
|
+
timestamp: Date.now()
|
|
237
|
+
});
|
|
238
|
+
})
|
|
239
|
+
.addRoute('/api-style-error', (ctx, res) => {
|
|
240
|
+
throw new Error('JSON error');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const handler = lambder.getHandler();
|
|
244
|
+
const result = await handler(createMockEvent('/api-style-error'), createMockContext());
|
|
245
|
+
|
|
246
|
+
const body = JSON.parse(result.body || '{}');
|
|
247
|
+
expect(body.error).toBe(true);
|
|
248
|
+
expect(body.message).toBe('JSON error');
|
|
249
|
+
expect(body.timestamp).toBeDefined();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Error Handling - Different Error Types', () => {
|
|
254
|
+
it('should handle Error objects', async () => {
|
|
255
|
+
const lambder = new Lambder({
|
|
256
|
+
publicPath: './public',
|
|
257
|
+
apiPath: '/api'
|
|
258
|
+
})
|
|
259
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
260
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
261
|
+
})
|
|
262
|
+
.addRoute('/test', (ctx, res) => {
|
|
263
|
+
throw new Error('Standard Error');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const handler = lambder.getHandler();
|
|
267
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
268
|
+
|
|
269
|
+
expect(result.body).toBe('Standard Error');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should handle TypeError', async () => {
|
|
273
|
+
const lambder = new Lambder({
|
|
274
|
+
publicPath: './public',
|
|
275
|
+
apiPath: '/api'
|
|
276
|
+
})
|
|
277
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
278
|
+
return res.raw({ statusCode: 500, body: `${err.name}: ${err.message}` });
|
|
279
|
+
})
|
|
280
|
+
.addRoute('/test', (ctx, res) => {
|
|
281
|
+
const obj: any = null;
|
|
282
|
+
obj.property.access; // Will throw TypeError
|
|
283
|
+
return res.html('Never reached');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const handler = lambder.getHandler();
|
|
287
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
288
|
+
|
|
289
|
+
expect(result.statusCode).toBe(500);
|
|
290
|
+
expect(result.body).toContain('Error');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle string throws as errors', async () => {
|
|
294
|
+
const lambder = new Lambder({
|
|
295
|
+
publicPath: './public',
|
|
296
|
+
apiPath: '/api'
|
|
297
|
+
})
|
|
298
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
299
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
300
|
+
})
|
|
301
|
+
.addRoute('/test', (ctx, res) => {
|
|
302
|
+
throw 'String error'; // Non-standard but should be handled
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const handler = lambder.getHandler();
|
|
306
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
307
|
+
|
|
308
|
+
expect(result.statusCode).toBe(500);
|
|
309
|
+
expect(result.body).toContain('String error');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Error Handling - Errors in Hooks', () => {
|
|
314
|
+
it('should catch errors in beforeRender hooks', async () => {
|
|
315
|
+
const lambder = new Lambder({
|
|
316
|
+
publicPath: './public',
|
|
317
|
+
apiPath: '/api'
|
|
318
|
+
})
|
|
319
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
320
|
+
return res.raw({ statusCode: 500, body: `Hook error: ${err.message}` });
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await lambder.addHook('beforeRender', async (ctx, res) => {
|
|
324
|
+
throw new Error('Before render failed');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
lambder.addRoute('/test', (ctx, res) => res.html('Test'));
|
|
328
|
+
|
|
329
|
+
const handler = lambder.getHandler();
|
|
330
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
331
|
+
|
|
332
|
+
expect(result.statusCode).toBe(500);
|
|
333
|
+
expect(result.body).toBe('Hook error: Before render failed');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should catch errors in afterRender hooks', async () => {
|
|
337
|
+
const lambder = new Lambder({
|
|
338
|
+
publicPath: './public',
|
|
339
|
+
apiPath: '/api'
|
|
340
|
+
})
|
|
341
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
342
|
+
return res.raw({ statusCode: 500, body: `Hook error: ${err.message}` });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
lambder.addRoute('/test', (ctx, res) => res.html('Test'));
|
|
346
|
+
|
|
347
|
+
await lambder.addHook('afterRender', async (ctx, res, response) => {
|
|
348
|
+
throw new Error('After render failed');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const handler = lambder.getHandler();
|
|
352
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
353
|
+
|
|
354
|
+
expect(result.statusCode).toBe(500);
|
|
355
|
+
expect(result.body).toBe('Hook error: After render failed');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should handle Error returned from beforeRender hook', async () => {
|
|
359
|
+
const lambder = new Lambder({
|
|
360
|
+
publicPath: './public',
|
|
361
|
+
apiPath: '/api'
|
|
362
|
+
})
|
|
363
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
364
|
+
return res.raw({ statusCode: 403, body: err.message });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await lambder.addHook('beforeRender', async (ctx, res) => {
|
|
368
|
+
return new Error('Access denied by hook');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
lambder.addRoute('/test', (ctx, res) => res.html('Test'));
|
|
372
|
+
|
|
373
|
+
const handler = lambder.getHandler();
|
|
374
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
375
|
+
|
|
376
|
+
expect(result.statusCode).toBe(403);
|
|
377
|
+
expect(result.body).toBe('Access denied by hook');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle Error returned from afterRender hook', async () => {
|
|
381
|
+
const lambder = new Lambder({
|
|
382
|
+
publicPath: './public',
|
|
383
|
+
apiPath: '/api'
|
|
384
|
+
})
|
|
385
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
386
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
lambder.addRoute('/test', (ctx, res) => res.html('Test'));
|
|
390
|
+
|
|
391
|
+
await lambder.addHook('afterRender', async (ctx, res, response) => {
|
|
392
|
+
return new Error('Response validation failed');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const handler = lambder.getHandler();
|
|
396
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
397
|
+
|
|
398
|
+
expect(result.statusCode).toBe(500);
|
|
399
|
+
expect(result.body).toBe('Response validation failed');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('Error Handling - Default Error Behavior', () => {
|
|
404
|
+
it('should return 500 when no global error handler is set', async () => {
|
|
405
|
+
const lambder = new Lambder({
|
|
406
|
+
publicPath: './public',
|
|
407
|
+
apiPath: '/api'
|
|
408
|
+
})
|
|
409
|
+
.addRoute('/error', (ctx, res) => {
|
|
410
|
+
throw new Error('Unhandled error');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const handler = lambder.getHandler();
|
|
414
|
+
const result = await handler(createMockEvent('/error'), createMockContext());
|
|
415
|
+
|
|
416
|
+
expect(result.statusCode).toBe(500);
|
|
417
|
+
expect(result.body).toBe('Internal Server Error.');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('Error Handling - Input Validation Errors', () => {
|
|
422
|
+
it('should return 400 for invalid API input', async () => {
|
|
423
|
+
const lambder = new Lambder({
|
|
424
|
+
publicPath: './public',
|
|
425
|
+
apiPath: '/api'
|
|
426
|
+
})
|
|
427
|
+
.addApi('testApi', {
|
|
428
|
+
input: z.object({
|
|
429
|
+
email: z.string().email(),
|
|
430
|
+
age: z.number().positive()
|
|
431
|
+
}),
|
|
432
|
+
output: z.object({ success: z.boolean() })
|
|
433
|
+
}, async (ctx, res) => {
|
|
434
|
+
return res.api({ success: true });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const handler = lambder.getHandler();
|
|
438
|
+
const event = createMockEvent('/api', 'POST', 'testApi', {
|
|
439
|
+
email: 'invalid-email',
|
|
440
|
+
age: -5
|
|
441
|
+
});
|
|
442
|
+
const result = await handler(event, createMockContext());
|
|
443
|
+
|
|
444
|
+
expect(result.statusCode).toBe(400);
|
|
445
|
+
const body = JSON.parse(result.body || '{}');
|
|
446
|
+
expect(body.error).toBe('Input validation failed');
|
|
447
|
+
expect(body.zodError).toBeDefined();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should allow custom handling of validation errors', async () => {
|
|
451
|
+
const lambder = new Lambder({
|
|
452
|
+
publicPath: './public',
|
|
453
|
+
apiPath: '/api'
|
|
454
|
+
})
|
|
455
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
456
|
+
// This won't be called for validation errors since they're handled before the handler
|
|
457
|
+
return res.raw({ statusCode: 500, body: err.message });
|
|
458
|
+
})
|
|
459
|
+
.addApi('testApi', {
|
|
460
|
+
input: z.object({ value: z.string().min(5) }),
|
|
461
|
+
output: z.object({ result: z.string() })
|
|
462
|
+
}, async (ctx, res) => {
|
|
463
|
+
return res.api({ result: 'success' });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const handler = lambder.getHandler();
|
|
467
|
+
const event = createMockEvent('/api', 'POST', 'testApi', { value: 'abc' }); // Too short
|
|
468
|
+
const result = await handler(event, createMockContext());
|
|
469
|
+
|
|
470
|
+
expect(result.statusCode).toBe(400);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('Error Handling - Error with Additional Context', () => {
|
|
475
|
+
it('should access request context in error handler', async () => {
|
|
476
|
+
let errorContext: any = null;
|
|
477
|
+
|
|
478
|
+
const lambder = new Lambder({
|
|
479
|
+
publicPath: './public',
|
|
480
|
+
apiPath: '/api'
|
|
481
|
+
})
|
|
482
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
483
|
+
errorContext = {
|
|
484
|
+
path: ctx?.path,
|
|
485
|
+
method: ctx?.method,
|
|
486
|
+
host: ctx?.host
|
|
487
|
+
};
|
|
488
|
+
return res.raw({ statusCode: 500, body: 'Error' });
|
|
489
|
+
})
|
|
490
|
+
.addRoute('/test', (ctx, res) => {
|
|
491
|
+
throw new Error('Test');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const handler = lambder.getHandler();
|
|
495
|
+
await handler(createMockEvent('/test'), createMockContext());
|
|
496
|
+
|
|
497
|
+
expect(errorContext.path).toBe('/test');
|
|
498
|
+
expect(errorContext.method).toBe('GET');
|
|
499
|
+
expect(errorContext.host).toBe('localhost');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should provide response builder in error handler', async () => {
|
|
503
|
+
const lambder = new Lambder({
|
|
504
|
+
publicPath: './public',
|
|
505
|
+
apiPath: '/api'
|
|
506
|
+
})
|
|
507
|
+
.setGlobalErrorHandler((err, ctx, responseBuilder) => {
|
|
508
|
+
// responseBuilder should have all response methods
|
|
509
|
+
return responseBuilder.json({
|
|
510
|
+
error: err.message,
|
|
511
|
+
code: 'CUSTOM_ERROR'
|
|
512
|
+
});
|
|
513
|
+
})
|
|
514
|
+
.addRoute('/test', (ctx, res) => {
|
|
515
|
+
throw new Error('Test error');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const handler = lambder.getHandler();
|
|
519
|
+
const result = await handler(createMockEvent('/test'), createMockContext());
|
|
520
|
+
|
|
521
|
+
const body = JSON.parse(result.body || '{}');
|
|
522
|
+
expect(body.error).toBe('Test error');
|
|
523
|
+
expect(body.code).toBe('CUSTOM_ERROR');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('Error Handling - Complex Error Scenarios', () => {
|
|
528
|
+
it('should handle errors in chained operations', async () => {
|
|
529
|
+
const lambder = new Lambder({
|
|
530
|
+
publicPath: './public',
|
|
531
|
+
apiPath: '/api'
|
|
532
|
+
})
|
|
533
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
534
|
+
return res.json({ error: err.message });
|
|
535
|
+
})
|
|
536
|
+
.addRoute('/chain-error', async (ctx, res) => {
|
|
537
|
+
await Promise.resolve()
|
|
538
|
+
.then(() => Promise.resolve())
|
|
539
|
+
.then(() => {
|
|
540
|
+
throw new Error('Chain error');
|
|
541
|
+
});
|
|
542
|
+
return res.html('Never reached');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const handler = lambder.getHandler();
|
|
546
|
+
const result = await handler(createMockEvent('/chain-error'), createMockContext());
|
|
547
|
+
|
|
548
|
+
const body = JSON.parse(result.body || '{}');
|
|
549
|
+
expect(body.error).toBe('Chain error');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('should distinguish between route errors and API errors', async () => {
|
|
553
|
+
const errorTypes: string[] = [];
|
|
554
|
+
|
|
555
|
+
const lambder = new Lambder({
|
|
556
|
+
publicPath: './public',
|
|
557
|
+
apiPath: '/api'
|
|
558
|
+
})
|
|
559
|
+
.setGlobalErrorHandler((err, ctx, res) => {
|
|
560
|
+
if (ctx?._otherInternal.isApiCall) {
|
|
561
|
+
errorTypes.push('api');
|
|
562
|
+
return res.api({ error: err.message });
|
|
563
|
+
} else {
|
|
564
|
+
errorTypes.push('route');
|
|
565
|
+
return res.html(`<h1>${err.message}</h1>`);
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
.addRoute('/route-error', (ctx, res) => {
|
|
569
|
+
throw new Error('Route error');
|
|
570
|
+
})
|
|
571
|
+
.addApi('errorApi', {
|
|
572
|
+
input: z.void(),
|
|
573
|
+
output: z.object({ error: z.string() })
|
|
574
|
+
}, async (ctx, res) => {
|
|
575
|
+
throw new Error('API error');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const handler = lambder.getHandler();
|
|
579
|
+
|
|
580
|
+
await handler(createMockEvent('/route-error'), createMockContext());
|
|
581
|
+
await handler(createMockEvent('/api', 'POST', 'errorApi', undefined), createMockContext());
|
|
582
|
+
|
|
583
|
+
expect(errorTypes).toEqual(['route', 'api']);
|
|
584
|
+
});
|
|
585
|
+
});
|