lambder 1.0.127 → 1.0.129
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/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -1
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +1 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +1 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -10,6 +10,7 @@ export type LambderSessionContext = {
|
|
|
10
10
|
data: any;
|
|
11
11
|
createdAt: number;
|
|
12
12
|
expiresAt: number;
|
|
13
|
+
lastAccessedAt: number;
|
|
13
14
|
ttlInSeconds: number;
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -20,22 +21,26 @@ export default class LambderSessionManager{
|
|
|
20
21
|
private partitionKey: string;
|
|
21
22
|
private sortKey: string;
|
|
22
23
|
private ddbDocumentClient: DynamoDBDocumentClient;
|
|
24
|
+
private enableSlidingExpiration: boolean;
|
|
23
25
|
|
|
24
26
|
constructor(
|
|
25
27
|
{
|
|
26
28
|
tableName, tableRegion,
|
|
27
29
|
partitionKey, sortKey,
|
|
28
30
|
sessionSalt,
|
|
31
|
+
enableSlidingExpiration = true,
|
|
29
32
|
}: {
|
|
30
33
|
tableName: string, tableRegion: string,
|
|
31
34
|
partitionKey: string, sortKey: string,
|
|
32
35
|
sessionSalt: string,
|
|
36
|
+
enableSlidingExpiration?: boolean,
|
|
33
37
|
}
|
|
34
38
|
){
|
|
35
39
|
this.tableName = tableName;
|
|
36
40
|
this.sessionSalt = sessionSalt;
|
|
37
41
|
this.partitionKey = partitionKey;
|
|
38
42
|
this.sortKey = sortKey;
|
|
43
|
+
this.enableSlidingExpiration = enableSlidingExpiration;
|
|
39
44
|
|
|
40
45
|
const ddbClient = new DynamoDBClient({ region: tableRegion });
|
|
41
46
|
this.ddbDocumentClient = DynamoDBDocumentClient.from(ddbClient);
|
|
@@ -47,6 +52,13 @@ export default class LambderSessionManager{
|
|
|
47
52
|
.digest("hex");
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
private constantTimeCompare(a: string, b: string): boolean {
|
|
56
|
+
if (a.length !== b.length) return false;
|
|
57
|
+
const bufferA = Buffer.from(a, 'utf8');
|
|
58
|
+
const bufferB = Buffer.from(b, 'utf8');
|
|
59
|
+
return crypto.timingSafeEqual(new Uint8Array(bufferA), new Uint8Array(bufferB));
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
private async ddbGetItem<T=any>(key:Record<string,string|number>){
|
|
51
63
|
const response = await this.ddbDocumentClient.send(
|
|
52
64
|
new GetCommand({ TableName: this.tableName, Key: key, ConsistentRead: true })
|
|
@@ -101,8 +113,9 @@ export default class LambderSessionManager{
|
|
|
101
113
|
const sessionKeyHash = this.sessionUserKeyHasher(sessionKey);
|
|
102
114
|
const sessionSortKey = crypto.randomBytes(32).toString("hex");
|
|
103
115
|
const sessionToken = `${sessionKeyHash}:${sessionSortKey}`;
|
|
104
|
-
const csrfToken = crypto.randomBytes(
|
|
116
|
+
const csrfToken = crypto.randomBytes(32).toString("hex");
|
|
105
117
|
const createdAt = Math.floor(Date.now()/1000);
|
|
118
|
+
const lastAccessedAt = createdAt;
|
|
106
119
|
const expiresAt = Number(createdAt) + Number(ttlInSeconds);
|
|
107
120
|
|
|
108
121
|
const session = {
|
|
@@ -110,7 +123,7 @@ export default class LambderSessionManager{
|
|
|
110
123
|
[this.sortKey]: sessionSortKey,
|
|
111
124
|
sessionToken, csrfToken,
|
|
112
125
|
sessionKey, data,
|
|
113
|
-
createdAt, expiresAt, ttlInSeconds
|
|
126
|
+
createdAt, lastAccessedAt, expiresAt, ttlInSeconds
|
|
114
127
|
};
|
|
115
128
|
await this.ddbPutItem(session);
|
|
116
129
|
return session;
|
|
@@ -122,6 +135,13 @@ export default class LambderSessionManager{
|
|
|
122
135
|
): Promise<LambderSessionContext> {
|
|
123
136
|
if(!session) throw new Error("Invalid session");
|
|
124
137
|
session.data = newData;
|
|
138
|
+
session.lastAccessedAt = Math.floor(Date.now()/1000);
|
|
139
|
+
|
|
140
|
+
// Update expiration if sliding expiration is enabled
|
|
141
|
+
if(this.enableSlidingExpiration){
|
|
142
|
+
session.expiresAt = session.lastAccessedAt + session.ttlInSeconds;
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
await this.ddbPutItem(session);
|
|
126
146
|
return session;
|
|
127
147
|
}
|
|
@@ -134,12 +154,23 @@ export default class LambderSessionManager{
|
|
|
134
154
|
[this.partitionKey]: sessionKeyHash,
|
|
135
155
|
[this.sortKey]: sessionSortKey
|
|
136
156
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if(!session
|
|
140
|
-
if(!session.
|
|
141
|
-
if(!session.
|
|
142
|
-
if(!session.
|
|
157
|
+
|
|
158
|
+
// Use constant error response to prevent timing attacks
|
|
159
|
+
if(!session) return null;
|
|
160
|
+
if(!session.sessionToken || !this.constantTimeCompare(session.sessionToken, sessionToken)) return null;
|
|
161
|
+
if(!session.csrfToken) return null;
|
|
162
|
+
if(!session.sessionKey) return null;
|
|
163
|
+
if(!session.createdAt) return null;
|
|
164
|
+
if(!session.expiresAt || session.expiresAt < Date.now()/1000) return null;
|
|
165
|
+
|
|
166
|
+
// Update last accessed time if sliding expiration is enabled
|
|
167
|
+
if(this.enableSlidingExpiration){
|
|
168
|
+
session.lastAccessedAt = Math.floor(Date.now()/1000);
|
|
169
|
+
session.expiresAt = session.lastAccessedAt + session.ttlInSeconds;
|
|
170
|
+
// Fire and forget - don't wait for the update
|
|
171
|
+
this.ddbPutItem(session).catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
return session;
|
|
144
175
|
}catch(err){
|
|
145
176
|
return null;
|
|
@@ -149,14 +180,14 @@ export default class LambderSessionManager{
|
|
|
149
180
|
public isSessionValid(session: any, sessionToken: any, csrfToken: any, skipCsrfTokenCheck = false){
|
|
150
181
|
if(!session) return false;
|
|
151
182
|
if(!sessionToken || typeof sessionToken !== "string") return false;
|
|
152
|
-
if(session.sessionToken
|
|
183
|
+
if(!this.constantTimeCompare(session.sessionToken, sessionToken)) return false;
|
|
153
184
|
if(!session.csrfToken) return false;
|
|
154
185
|
if(!session.sessionKey) return false;
|
|
155
186
|
if(!session.createdAt) return false;
|
|
156
187
|
if(!session.expiresAt || session.expiresAt < Date.now()/1000) return false;
|
|
157
188
|
if(!skipCsrfTokenCheck){
|
|
158
189
|
if(!csrfToken || typeof csrfToken !== "string") return false;
|
|
159
|
-
if(session.csrfToken
|
|
190
|
+
if(!this.constantTimeCompare(session.csrfToken, csrfToken)) return false;
|
|
160
191
|
}
|
|
161
192
|
return true;
|
|
162
193
|
}
|
|
@@ -173,4 +204,14 @@ export default class LambderSessionManager{
|
|
|
173
204
|
await this.ddbDeleteAllByPartitionKey(session[this.partitionKey])
|
|
174
205
|
return true;
|
|
175
206
|
};
|
|
207
|
+
|
|
208
|
+
public async regenerateSession(session: LambderSessionContext): Promise<LambderSessionContext> {
|
|
209
|
+
if(!session) throw new Error("Invalid session");
|
|
210
|
+
|
|
211
|
+
// Delete old session
|
|
212
|
+
await this.deleteSession(session);
|
|
213
|
+
|
|
214
|
+
// Create new session with same sessionKey and data but new tokens
|
|
215
|
+
return await this.createSession(session.sessionKey, session.data, session.ttlInSeconds);
|
|
216
|
+
}
|
|
176
217
|
};
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Type Enforcement Runtime Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the typed APIs work correctly at runtime
|
|
5
|
+
* while maintaining type safety at compile time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
|
|
10
|
+
import Lambder from '../src/Lambder.js';
|
|
11
|
+
import type { ApiContract } from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
// Mock AWS Lambda event and context
|
|
14
|
+
const createMockEvent = (apiName: string, payload: any): APIGatewayProxyEvent => ({
|
|
15
|
+
body: JSON.stringify({ apiName, payload }),
|
|
16
|
+
headers: { Host: 'localhost' },
|
|
17
|
+
multiValueHeaders: {},
|
|
18
|
+
httpMethod: 'POST',
|
|
19
|
+
isBase64Encoded: false,
|
|
20
|
+
path: '/api',
|
|
21
|
+
pathParameters: null,
|
|
22
|
+
queryStringParameters: null,
|
|
23
|
+
multiValueQueryStringParameters: null,
|
|
24
|
+
stageVariables: null,
|
|
25
|
+
requestContext: {} as any,
|
|
26
|
+
resource: '',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createMockContext = (): Context => ({
|
|
30
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
31
|
+
functionName: 'test',
|
|
32
|
+
functionVersion: '1',
|
|
33
|
+
invokedFunctionArn: 'arn',
|
|
34
|
+
memoryLimitInMB: '128',
|
|
35
|
+
awsRequestId: '123',
|
|
36
|
+
logGroupName: 'group',
|
|
37
|
+
logStreamName: 'stream',
|
|
38
|
+
getRemainingTimeInMillis: () => 1000,
|
|
39
|
+
done: () => {},
|
|
40
|
+
fail: () => {},
|
|
41
|
+
succeed: () => {},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Test contract
|
|
45
|
+
type TestContract = ApiContract<{
|
|
46
|
+
echo: { input: { message: string }, output: { echo: string } },
|
|
47
|
+
add: { input: { a: number, b: number }, output: number },
|
|
48
|
+
getUser: { input: { userId: string }, output: { id: string, name: string, age: number } },
|
|
49
|
+
listUsers: { input: void, output: Array<{ id: string, name: string }> },
|
|
50
|
+
findUser: { input: { email: string }, output: { id: string, name: string } | null },
|
|
51
|
+
deleteUser: { input: { userId: string }, output: boolean },
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Runtime Tests
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe('Output Type Enforcement - Runtime', () => {
|
|
59
|
+
it('should return correct primitive types', async () => {
|
|
60
|
+
const lambder = new Lambder<TestContract>({
|
|
61
|
+
publicPath: './public',
|
|
62
|
+
apiPath: '/api',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
66
|
+
const { a, b } = ctx.apiPayload;
|
|
67
|
+
const sum = a + b;
|
|
68
|
+
return resolver.api(sum);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const event = createMockEvent('add', { a: 5, b: 3 });
|
|
72
|
+
const context = createMockContext();
|
|
73
|
+
const response = await lambder.render(event, context);
|
|
74
|
+
|
|
75
|
+
expect(response.statusCode).toBe(200);
|
|
76
|
+
const body = JSON.parse(response.body || '{}');
|
|
77
|
+
expect(body.payload).toBe(8);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return correct object types', async () => {
|
|
81
|
+
const lambder = new Lambder<TestContract>({
|
|
82
|
+
publicPath: './public',
|
|
83
|
+
apiPath: '/api',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
87
|
+
const userId = ctx.apiPayload.userId;
|
|
88
|
+
return resolver.api({
|
|
89
|
+
id: userId,
|
|
90
|
+
name: 'John Doe',
|
|
91
|
+
age: 30
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const event = createMockEvent('getUser', { userId: '123' });
|
|
96
|
+
const context = createMockContext();
|
|
97
|
+
const response = await lambder.render(event, context);
|
|
98
|
+
|
|
99
|
+
expect(response.statusCode).toBe(200);
|
|
100
|
+
const body = JSON.parse(response.body || '{}');
|
|
101
|
+
expect(body.payload).toEqual({
|
|
102
|
+
id: '123',
|
|
103
|
+
name: 'John Doe',
|
|
104
|
+
age: 30
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return correct array types', async () => {
|
|
109
|
+
const lambder = new Lambder<TestContract>({
|
|
110
|
+
publicPath: './public',
|
|
111
|
+
apiPath: '/api',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
115
|
+
return resolver.api([
|
|
116
|
+
{ id: '1', name: 'Alice' },
|
|
117
|
+
{ id: '2', name: 'Bob' }
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const event = createMockEvent('listUsers', undefined);
|
|
122
|
+
const context = createMockContext();
|
|
123
|
+
const response = await lambder.render(event, context);
|
|
124
|
+
|
|
125
|
+
expect(response.statusCode).toBe(200);
|
|
126
|
+
const body = JSON.parse(response.body || '{}');
|
|
127
|
+
expect(body.payload).toEqual([
|
|
128
|
+
{ id: '1', name: 'Alice' },
|
|
129
|
+
{ id: '2', name: 'Bob' }
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle null returns correctly', async () => {
|
|
134
|
+
const lambder = new Lambder<TestContract>({
|
|
135
|
+
publicPath: './public',
|
|
136
|
+
apiPath: '/api',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
140
|
+
const email = ctx.apiPayload.email;
|
|
141
|
+
if (email === 'notfound@example.com') {
|
|
142
|
+
return resolver.api(null);
|
|
143
|
+
}
|
|
144
|
+
return resolver.api({ id: '1', name: 'Found User' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Test null case
|
|
148
|
+
const event1 = createMockEvent('findUser', { email: 'notfound@example.com' });
|
|
149
|
+
const context1 = createMockContext();
|
|
150
|
+
const response1 = await lambder.render(event1, context1);
|
|
151
|
+
|
|
152
|
+
expect(response1.statusCode).toBe(200);
|
|
153
|
+
const body1 = JSON.parse(response1.body || '{}');
|
|
154
|
+
expect(body1.payload).toBeNull();
|
|
155
|
+
|
|
156
|
+
// Test found case
|
|
157
|
+
const event2 = createMockEvent('findUser', { email: 'found@example.com' });
|
|
158
|
+
const context2 = createMockContext();
|
|
159
|
+
const response2 = await lambder.render(event2, context2);
|
|
160
|
+
|
|
161
|
+
expect(response2.statusCode).toBe(200);
|
|
162
|
+
const body2 = JSON.parse(response2.body || '{}');
|
|
163
|
+
expect(body2.payload).toEqual({ id: '1', name: 'Found User' });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return boolean types correctly', async () => {
|
|
167
|
+
const lambder = new Lambder<TestContract>({
|
|
168
|
+
publicPath: './public',
|
|
169
|
+
apiPath: '/api',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
173
|
+
const userId = ctx.apiPayload.userId;
|
|
174
|
+
// Mock deletion
|
|
175
|
+
const success = userId !== '';
|
|
176
|
+
return resolver.api(success);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const event = createMockEvent('deleteUser', { userId: '123' });
|
|
180
|
+
const context = createMockContext();
|
|
181
|
+
const response = await lambder.render(event, context);
|
|
182
|
+
|
|
183
|
+
expect(response.statusCode).toBe(200);
|
|
184
|
+
const body = JSON.parse(response.body || '{}');
|
|
185
|
+
expect(body.payload).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should work with die.api()', async () => {
|
|
189
|
+
const lambder = new Lambder<TestContract>({
|
|
190
|
+
publicPath: './public',
|
|
191
|
+
apiPath: '/api',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
lambder.addApi('echo', async (ctx, resolver) => {
|
|
195
|
+
return resolver.die.api({ echo: ctx.apiPayload.message });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const event = createMockEvent('echo', { message: 'Hello!' });
|
|
199
|
+
const context = createMockContext();
|
|
200
|
+
const response = await lambder.render(event, context);
|
|
201
|
+
|
|
202
|
+
expect(response.statusCode).toBe(200);
|
|
203
|
+
const body = JSON.parse(response.body || '{}');
|
|
204
|
+
expect(body.payload).toEqual({ echo: 'Hello!' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should work with session APIs', async () => {
|
|
208
|
+
const lambder = new Lambder<TestContract>({
|
|
209
|
+
publicPath: './public',
|
|
210
|
+
apiPath: '/api',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Note: Session will fail without DynamoDB setup, but we're testing type enforcement
|
|
214
|
+
lambder.addSessionApi('getUser', async (ctx, resolver) => {
|
|
215
|
+
return resolver.api({
|
|
216
|
+
id: ctx.apiPayload.userId,
|
|
217
|
+
name: 'Session User',
|
|
218
|
+
age: 25
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// This will return 500 error because session is not configured
|
|
223
|
+
const event = createMockEvent('getUser', { userId: '123' });
|
|
224
|
+
const context = createMockContext();
|
|
225
|
+
|
|
226
|
+
const response = await lambder.render(event, context);
|
|
227
|
+
|
|
228
|
+
// Should return 500 error when session is not configured
|
|
229
|
+
expect(response.statusCode).toBe(500);
|
|
230
|
+
expect(response.body).toBe('Internal Server Error.');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Input Type Tests
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
describe('Input Type Enforcement - Runtime', () => {
|
|
239
|
+
it('should receive correctly typed input', async () => {
|
|
240
|
+
const lambder = new Lambder<TestContract>({
|
|
241
|
+
publicPath: './public',
|
|
242
|
+
apiPath: '/api',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
lambder.addApi('echo', async (ctx, resolver) => {
|
|
246
|
+
// Verify input is correctly typed at runtime
|
|
247
|
+
const message: string = ctx.apiPayload.message;
|
|
248
|
+
expect(typeof message).toBe('string');
|
|
249
|
+
return resolver.api({ echo: message });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const event = createMockEvent('echo', { message: 'Test Message' });
|
|
253
|
+
const context = createMockContext();
|
|
254
|
+
await lambder.render(event, context);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle void input correctly', async () => {
|
|
258
|
+
const lambder = new Lambder<TestContract>({
|
|
259
|
+
publicPath: './public',
|
|
260
|
+
apiPath: '/api',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
264
|
+
// apiPayload should be undefined for void input
|
|
265
|
+
expect(ctx.apiPayload).toBeUndefined();
|
|
266
|
+
return resolver.api([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const event = createMockEvent('listUsers', undefined);
|
|
270
|
+
const context = createMockContext();
|
|
271
|
+
await lambder.render(event, context);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should handle complex input objects', async () => {
|
|
275
|
+
const lambder = new Lambder<TestContract>({
|
|
276
|
+
publicPath: './public',
|
|
277
|
+
apiPath: '/api',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
281
|
+
const { a, b } = ctx.apiPayload;
|
|
282
|
+
expect(typeof a).toBe('number');
|
|
283
|
+
expect(typeof b).toBe('number');
|
|
284
|
+
return resolver.api(a + b);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const event = createMockEvent('add', { a: 10, b: 20 });
|
|
288
|
+
const context = createMockContext();
|
|
289
|
+
const response = await lambder.render(event, context);
|
|
290
|
+
|
|
291
|
+
const body = JSON.parse(response.body || '{}');
|
|
292
|
+
expect(body.payload).toBe(30);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Edge Cases
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
describe('Edge Cases', () => {
|
|
301
|
+
it('should handle empty arrays', async () => {
|
|
302
|
+
const lambder = new Lambder<TestContract>({
|
|
303
|
+
publicPath: './public',
|
|
304
|
+
apiPath: '/api',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
308
|
+
return resolver.api([]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const event = createMockEvent('listUsers', undefined);
|
|
312
|
+
const context = createMockContext();
|
|
313
|
+
const response = await lambder.render(event, context);
|
|
314
|
+
|
|
315
|
+
expect(response.statusCode).toBe(200);
|
|
316
|
+
const body = JSON.parse(response.body || '{}');
|
|
317
|
+
expect(body.payload).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should handle zero as a valid number', async () => {
|
|
321
|
+
const lambder = new Lambder<TestContract>({
|
|
322
|
+
publicPath: './public',
|
|
323
|
+
apiPath: '/api',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
327
|
+
return resolver.api(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const event = createMockEvent('add', { a: 0, b: 0 });
|
|
331
|
+
const context = createMockContext();
|
|
332
|
+
const response = await lambder.render(event, context);
|
|
333
|
+
|
|
334
|
+
expect(response.statusCode).toBe(200);
|
|
335
|
+
const body = JSON.parse(response.body || '{}');
|
|
336
|
+
expect(body.payload).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle empty strings in objects', async () => {
|
|
340
|
+
const lambder = new Lambder<TestContract>({
|
|
341
|
+
publicPath: './public',
|
|
342
|
+
apiPath: '/api',
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
346
|
+
return resolver.api({
|
|
347
|
+
id: '',
|
|
348
|
+
name: '',
|
|
349
|
+
age: 0
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const event = createMockEvent('getUser', { userId: '' });
|
|
354
|
+
const context = createMockContext();
|
|
355
|
+
const response = await lambder.render(event, context);
|
|
356
|
+
|
|
357
|
+
expect(response.statusCode).toBe(200);
|
|
358
|
+
const body = JSON.parse(response.body || '{}');
|
|
359
|
+
expect(body.payload).toEqual({
|
|
360
|
+
id: '',
|
|
361
|
+
name: '',
|
|
362
|
+
age: 0
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|