@hazeljs/serverless 0.2.0-beta.54 → 0.2.0-beta.56
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 +1 -1
- package/dist/adapters.test.d.ts +2 -0
- package/dist/adapters.test.d.ts.map +1 -0
- package/dist/adapters.test.js +416 -0
- package/dist/serverless.test.d.ts +2 -0
- package/dist/serverless.test.d.ts.map +1 -0
- package/dist/serverless.test.js +246 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -498,4 +498,4 @@ Apache 2.0 © [HazelJS](https://hazeljs.com)
|
|
|
498
498
|
- [Google Cloud Functions Docs](https://cloud.google.com/functions/docs)
|
|
499
499
|
- [GitHub](https://github.com/hazel-js/hazeljs)
|
|
500
500
|
- [Issues](https://github.com/hazel-js/hazeljs/issues)
|
|
501
|
-
- [Discord](https://discord.
|
|
501
|
+
- [Discord](https://discord.com/channels/1448263814238965833/1448263814859456575)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.test.d.ts","sourceRoot":"","sources":["../src/adapters.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
jest.mock('@hazeljs/core', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
HazelApp: jest.fn(),
|
|
12
|
+
Container: { getInstance: jest.fn().mockReturnValue({}) },
|
|
13
|
+
Injectable: () => () => undefined,
|
|
14
|
+
HazelModule: () => () => undefined,
|
|
15
|
+
Type: null,
|
|
16
|
+
default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
17
|
+
}));
|
|
18
|
+
const core_1 = require("@hazeljs/core");
|
|
19
|
+
const lambda_adapter_1 = require("./lambda.adapter");
|
|
20
|
+
const cloud_function_adapter_1 = require("./cloud-function.adapter");
|
|
21
|
+
const cold_start_optimizer_1 = require("./cold-start.optimizer");
|
|
22
|
+
const serverless_decorator_1 = require("./serverless.decorator");
|
|
23
|
+
class MockModule {
|
|
24
|
+
}
|
|
25
|
+
function makeEvent(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
httpMethod: 'GET',
|
|
28
|
+
path: '/test',
|
|
29
|
+
headers: {},
|
|
30
|
+
queryStringParameters: null,
|
|
31
|
+
body: null,
|
|
32
|
+
isBase64Encoded: false,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function makeContext(overrides = {}) {
|
|
37
|
+
return {
|
|
38
|
+
awsRequestId: 'req-123',
|
|
39
|
+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:000:function:test',
|
|
40
|
+
functionName: 'test-fn',
|
|
41
|
+
functionVersion: '$LATEST',
|
|
42
|
+
logGroupName: '/aws/lambda/test',
|
|
43
|
+
logStreamName: '2024/01/01/test',
|
|
44
|
+
memoryLimitInMB: '512',
|
|
45
|
+
getRemainingTimeInMillis: () => 30000,
|
|
46
|
+
done: jest.fn(),
|
|
47
|
+
fail: jest.fn(),
|
|
48
|
+
succeed: jest.fn(),
|
|
49
|
+
callbackWaitsForEmptyEventLoop: true,
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
describe('LambdaAdapter', () => {
|
|
54
|
+
let mockMatch;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.clearAllMocks();
|
|
57
|
+
mockMatch = jest.fn().mockResolvedValue(null);
|
|
58
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
59
|
+
getRouter: jest.fn().mockReturnValue({ match: mockMatch }),
|
|
60
|
+
}));
|
|
61
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
62
|
+
});
|
|
63
|
+
describe('createHandler() – cold start tracking', () => {
|
|
64
|
+
it('isCold() is true before first call', () => {
|
|
65
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
66
|
+
expect(adapter.isCold()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it('isCold() becomes false after first handler call', async () => {
|
|
69
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
70
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
71
|
+
expect(adapter.isCold()).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('getApp() is undefined before handler is called', () => {
|
|
74
|
+
expect(new lambda_adapter_1.LambdaAdapter(MockModule).getApp()).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it('getApp() returns HazelApp after first call', async () => {
|
|
77
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
78
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
79
|
+
expect(adapter.getApp()).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('createHandler() – routing', () => {
|
|
83
|
+
it('returns 404 when route is not found', async () => {
|
|
84
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
85
|
+
const result = await adapter.createHandler()(makeEvent({ path: '/missing' }), makeContext());
|
|
86
|
+
expect(result.statusCode).toBe(404);
|
|
87
|
+
expect(JSON.parse(result.body)).toMatchObject({ message: 'Route not found' });
|
|
88
|
+
});
|
|
89
|
+
it('calls matched route handler and returns json response', async () => {
|
|
90
|
+
mockMatch.mockResolvedValue({
|
|
91
|
+
handler: async (_req, res) => res.json({ ok: true }),
|
|
92
|
+
});
|
|
93
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
94
|
+
expect(result.statusCode).toBe(200);
|
|
95
|
+
expect(JSON.parse(result.body)).toEqual({ ok: true });
|
|
96
|
+
});
|
|
97
|
+
it('uses returned value from handler when res methods are not called', async () => {
|
|
98
|
+
mockMatch.mockResolvedValue({ handler: async () => ({ direct: 'return' }) });
|
|
99
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
100
|
+
expect(JSON.parse(result.body)).toEqual({ direct: 'return' });
|
|
101
|
+
});
|
|
102
|
+
it('respects res.status() code', async () => {
|
|
103
|
+
mockMatch.mockResolvedValue({
|
|
104
|
+
handler: async (_req, res) => res.status(201).json({ created: true }),
|
|
105
|
+
});
|
|
106
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST' }), makeContext());
|
|
107
|
+
expect(result.statusCode).toBe(201);
|
|
108
|
+
});
|
|
109
|
+
it('returns string body from res.send()', async () => {
|
|
110
|
+
mockMatch.mockResolvedValue({
|
|
111
|
+
handler: async (_req, res) => res.send('plain text'),
|
|
112
|
+
});
|
|
113
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
114
|
+
expect(result.body).toBe('plain text');
|
|
115
|
+
});
|
|
116
|
+
it('supports res.setHeader()', async () => {
|
|
117
|
+
mockMatch.mockResolvedValue({
|
|
118
|
+
handler: async (_req, res) => {
|
|
119
|
+
res.setHeader('X-Custom', 'value');
|
|
120
|
+
res.json({});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
124
|
+
expect(result.headers?.['X-Custom']).toBe('value');
|
|
125
|
+
});
|
|
126
|
+
it('returns error statusCode from thrown error with statusCode property', async () => {
|
|
127
|
+
const err = Object.assign(new Error('Forbidden'), { statusCode: 403 });
|
|
128
|
+
mockMatch.mockRejectedValue(err);
|
|
129
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
130
|
+
expect(result.statusCode).toBe(403);
|
|
131
|
+
});
|
|
132
|
+
it('returns 500 when route handler throws without statusCode', async () => {
|
|
133
|
+
mockMatch.mockRejectedValue(new Error('Unexpected'));
|
|
134
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
135
|
+
expect(result.statusCode).toBe(500);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('createHandler() – request conversion', () => {
|
|
139
|
+
it('parses JSON body', async () => {
|
|
140
|
+
let capturedBody;
|
|
141
|
+
mockMatch.mockResolvedValue({
|
|
142
|
+
handler: async (req, res) => {
|
|
143
|
+
capturedBody = req.body;
|
|
144
|
+
res.json({});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: '{"key":"val"}' }), makeContext());
|
|
148
|
+
expect(capturedBody).toEqual({ key: 'val' });
|
|
149
|
+
});
|
|
150
|
+
it('parses base64-encoded JSON body', async () => {
|
|
151
|
+
let capturedBody;
|
|
152
|
+
mockMatch.mockResolvedValue({
|
|
153
|
+
handler: async (req, res) => {
|
|
154
|
+
capturedBody = req.body;
|
|
155
|
+
res.json({});
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const b64 = Buffer.from(JSON.stringify({ encoded: true })).toString('base64');
|
|
159
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: b64, isBase64Encoded: true }), makeContext());
|
|
160
|
+
expect(capturedBody).toEqual({ encoded: true });
|
|
161
|
+
});
|
|
162
|
+
it('returns raw string body when JSON parse fails', async () => {
|
|
163
|
+
let capturedBody;
|
|
164
|
+
mockMatch.mockResolvedValue({
|
|
165
|
+
handler: async (req, res) => {
|
|
166
|
+
capturedBody = req.body;
|
|
167
|
+
res.json({});
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ httpMethod: 'POST', body: 'not-valid-json' }), makeContext());
|
|
171
|
+
expect(capturedBody).toBe('not-valid-json');
|
|
172
|
+
});
|
|
173
|
+
it('passes query string parameters to handler', async () => {
|
|
174
|
+
let capturedQuery;
|
|
175
|
+
mockMatch.mockResolvedValue({
|
|
176
|
+
handler: async (req, res) => {
|
|
177
|
+
capturedQuery = req.query;
|
|
178
|
+
res.json({});
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ queryStringParameters: { page: '2', size: '10' } }), makeContext());
|
|
182
|
+
expect(capturedQuery).toEqual({ page: '2', size: '10' });
|
|
183
|
+
});
|
|
184
|
+
it('passes pathParameters to handler', async () => {
|
|
185
|
+
let capturedParams;
|
|
186
|
+
mockMatch.mockResolvedValue({
|
|
187
|
+
handler: async (req, res) => {
|
|
188
|
+
capturedParams = req.params;
|
|
189
|
+
res.json({});
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent({ pathParameters: { id: '42' } }), makeContext());
|
|
193
|
+
expect(capturedParams).toEqual({ id: '42' });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('createHandler() – initialization errors', () => {
|
|
197
|
+
it('returns 500 when HazelApp constructor throws', async () => {
|
|
198
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
199
|
+
throw new Error('Module init failed');
|
|
200
|
+
});
|
|
201
|
+
const result = await new lambda_adapter_1.LambdaAdapter(MockModule).createHandler()(makeEvent(), makeContext());
|
|
202
|
+
expect(result.statusCode).toBe(500);
|
|
203
|
+
expect(JSON.parse(result.body).message).toBe('Module init failed');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('createHandler() – app reuse', () => {
|
|
207
|
+
it('creates HazelApp only once across multiple calls', async () => {
|
|
208
|
+
mockMatch.mockResolvedValue({
|
|
209
|
+
handler: async (_r, res) => res.json({}),
|
|
210
|
+
});
|
|
211
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(MockModule);
|
|
212
|
+
const handler = adapter.createHandler();
|
|
213
|
+
await handler(makeEvent(), makeContext());
|
|
214
|
+
await handler(makeEvent(), makeContext());
|
|
215
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('cold start optimization', () => {
|
|
219
|
+
it('calls warmUp when module has @Serverless coldStartOptimization:true', async () => {
|
|
220
|
+
let OptimizedModule = class OptimizedModule {
|
|
221
|
+
};
|
|
222
|
+
OptimizedModule = __decorate([
|
|
223
|
+
(0, serverless_decorator_1.Serverless)({ coldStartOptimization: true })
|
|
224
|
+
], OptimizedModule);
|
|
225
|
+
mockMatch.mockResolvedValue({
|
|
226
|
+
handler: async (_r, res) => res.json({}),
|
|
227
|
+
});
|
|
228
|
+
const adapter = new lambda_adapter_1.LambdaAdapter(OptimizedModule);
|
|
229
|
+
await adapter.createHandler()(makeEvent(), makeContext());
|
|
230
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('createLambdaHandler()', () => {
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
jest.clearAllMocks();
|
|
237
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
238
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn().mockResolvedValue(null) }),
|
|
239
|
+
}));
|
|
240
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
241
|
+
});
|
|
242
|
+
it('returns a callable handler that produces a ServerlessResponse', async () => {
|
|
243
|
+
const handler = (0, lambda_adapter_1.createLambdaHandler)(MockModule);
|
|
244
|
+
const result = await handler(makeEvent(), makeContext());
|
|
245
|
+
expect(result).toHaveProperty('statusCode');
|
|
246
|
+
expect(result).toHaveProperty('body');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// ─── CloudFunctionAdapter ────────────────────────────────────────────────────
|
|
250
|
+
function makeCloudReq(overrides = {}) {
|
|
251
|
+
return {
|
|
252
|
+
method: 'GET',
|
|
253
|
+
url: '/test',
|
|
254
|
+
path: '/test',
|
|
255
|
+
headers: { 'content-type': 'application/json' },
|
|
256
|
+
query: {},
|
|
257
|
+
...overrides,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function makeCloudRes() {
|
|
261
|
+
const captures = {
|
|
262
|
+
headers: {},
|
|
263
|
+
};
|
|
264
|
+
const res = {
|
|
265
|
+
_captures: captures,
|
|
266
|
+
status: jest.fn().mockReturnThis(),
|
|
267
|
+
set: jest.fn().mockReturnThis(),
|
|
268
|
+
send: jest.fn().mockImplementation((b) => {
|
|
269
|
+
captures.body = b;
|
|
270
|
+
}),
|
|
271
|
+
json: jest.fn().mockImplementation((b) => {
|
|
272
|
+
captures.body = b;
|
|
273
|
+
}),
|
|
274
|
+
end: jest.fn(),
|
|
275
|
+
};
|
|
276
|
+
return res;
|
|
277
|
+
}
|
|
278
|
+
describe('CloudFunctionAdapter', () => {
|
|
279
|
+
let mockMatch;
|
|
280
|
+
beforeEach(() => {
|
|
281
|
+
jest.clearAllMocks();
|
|
282
|
+
mockMatch = jest.fn().mockResolvedValue(null);
|
|
283
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
284
|
+
getRouter: jest.fn().mockReturnValue({ match: mockMatch }),
|
|
285
|
+
}));
|
|
286
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
287
|
+
});
|
|
288
|
+
describe('createHttpHandler()', () => {
|
|
289
|
+
it('calls res.status(404) when route not found', async () => {
|
|
290
|
+
const res = makeCloudRes();
|
|
291
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
292
|
+
expect(res.status).toHaveBeenCalledWith(404);
|
|
293
|
+
expect(res.send).toHaveBeenCalled();
|
|
294
|
+
});
|
|
295
|
+
it('calls matched route and sends response', async () => {
|
|
296
|
+
mockMatch.mockResolvedValue({
|
|
297
|
+
handler: async (_r, synRes) => synRes.json({ ok: true }),
|
|
298
|
+
});
|
|
299
|
+
const res = makeCloudRes();
|
|
300
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
301
|
+
expect(res.send).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
it('calls res.status(500).json() on handler init error', async () => {
|
|
304
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
305
|
+
throw new Error('crash');
|
|
306
|
+
});
|
|
307
|
+
const res = makeCloudRes();
|
|
308
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
309
|
+
expect(res.status).toHaveBeenCalledWith(500);
|
|
310
|
+
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'Internal Server Error' }));
|
|
311
|
+
});
|
|
312
|
+
it('normalizes string[] headers to first value', async () => {
|
|
313
|
+
let capturedHeaders;
|
|
314
|
+
mockMatch.mockResolvedValue({
|
|
315
|
+
handler: async (req, res) => {
|
|
316
|
+
capturedHeaders = req.headers;
|
|
317
|
+
res.json({});
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const res = makeCloudRes();
|
|
321
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq({ headers: { 'x-multi': ['alpha', 'beta'] } }), res);
|
|
322
|
+
expect(capturedHeaders?.['x-multi']).toBe('alpha');
|
|
323
|
+
});
|
|
324
|
+
it('normalizes string[] query params to first value', async () => {
|
|
325
|
+
let capturedQuery;
|
|
326
|
+
mockMatch.mockResolvedValue({
|
|
327
|
+
handler: async (req, res) => {
|
|
328
|
+
capturedQuery = req.query;
|
|
329
|
+
res.json({});
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
const res = makeCloudRes();
|
|
333
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq({ query: { tag: ['first', 'second'] } }), res);
|
|
334
|
+
expect(capturedQuery?.tag).toBe('first');
|
|
335
|
+
});
|
|
336
|
+
it('sets headers on response from internal ServerlessResponse headers', async () => {
|
|
337
|
+
mockMatch.mockResolvedValue({
|
|
338
|
+
handler: async (_r, synRes) => {
|
|
339
|
+
synRes.setHeader('X-Trace', 'abc');
|
|
340
|
+
synRes.json({});
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const res = makeCloudRes();
|
|
344
|
+
await new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createHttpHandler()(makeCloudReq(), res);
|
|
345
|
+
expect(res.set).toHaveBeenCalledWith('X-Trace', 'abc');
|
|
346
|
+
});
|
|
347
|
+
it('isCold() becomes false after first call', async () => {
|
|
348
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
349
|
+
expect(adapter.isCold()).toBe(true);
|
|
350
|
+
const res = makeCloudRes();
|
|
351
|
+
await adapter.createHttpHandler()(makeCloudReq(), res);
|
|
352
|
+
expect(adapter.isCold()).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
it('getApp() returns undefined before first call', () => {
|
|
355
|
+
expect(new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).getApp()).toBeUndefined();
|
|
356
|
+
});
|
|
357
|
+
it('creates HazelApp only once across multiple calls', async () => {
|
|
358
|
+
mockMatch.mockResolvedValue({
|
|
359
|
+
handler: async (_r, res) => res.json({}),
|
|
360
|
+
});
|
|
361
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
362
|
+
const handler = adapter.createHttpHandler();
|
|
363
|
+
const res1 = makeCloudRes();
|
|
364
|
+
const res2 = makeCloudRes();
|
|
365
|
+
await handler(makeCloudReq(), res1);
|
|
366
|
+
await handler(makeCloudReq(), res2);
|
|
367
|
+
expect(core_1.HazelApp).toHaveBeenCalledTimes(1);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
describe('createEventHandler()', () => {
|
|
371
|
+
it('resolves without error for a valid event', async () => {
|
|
372
|
+
const handler = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createEventHandler();
|
|
373
|
+
await expect(handler({ data: 'payload' }, { eventType: 'pubsub', resource: 'projects/test' })).resolves.toBeUndefined();
|
|
374
|
+
});
|
|
375
|
+
it('rethrows errors from initialization', async () => {
|
|
376
|
+
core_1.HazelApp.mockImplementationOnce(() => {
|
|
377
|
+
throw new Error('init failed');
|
|
378
|
+
});
|
|
379
|
+
const handler = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule).createEventHandler();
|
|
380
|
+
await expect(handler({}, {})).rejects.toThrow('init failed');
|
|
381
|
+
});
|
|
382
|
+
it('isCold() becomes false after first event call', async () => {
|
|
383
|
+
const adapter = new cloud_function_adapter_1.CloudFunctionAdapter(MockModule);
|
|
384
|
+
await adapter.createEventHandler()({}, {});
|
|
385
|
+
expect(adapter.isCold()).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('createCloudFunctionHandler()', () => {
|
|
390
|
+
beforeEach(() => {
|
|
391
|
+
jest.clearAllMocks();
|
|
392
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
393
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn().mockResolvedValue(null) }),
|
|
394
|
+
}));
|
|
395
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
396
|
+
});
|
|
397
|
+
it('returns a callable HTTP handler', async () => {
|
|
398
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionHandler)(MockModule);
|
|
399
|
+
const res = makeCloudRes();
|
|
400
|
+
await handler(makeCloudReq(), res);
|
|
401
|
+
expect(res.send).toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
describe('createCloudFunctionEventHandler()', () => {
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
jest.clearAllMocks();
|
|
407
|
+
core_1.HazelApp.mockImplementation(() => ({
|
|
408
|
+
getRouter: jest.fn().mockReturnValue({ match: jest.fn() }),
|
|
409
|
+
}));
|
|
410
|
+
cold_start_optimizer_1.ColdStartOptimizer.getInstance().reset();
|
|
411
|
+
});
|
|
412
|
+
it('returns a callable event handler', async () => {
|
|
413
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionEventHandler)(MockModule);
|
|
414
|
+
await expect(handler({ data: 'test' }, {})).resolves.toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverless.test.d.ts","sourceRoot":"","sources":["../src/serverless.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const serverless_decorator_1 = require("./serverless.decorator");
|
|
13
|
+
const cold_start_optimizer_1 = require("./cold-start.optimizer");
|
|
14
|
+
const lambda_adapter_1 = require("./lambda.adapter");
|
|
15
|
+
const cloud_function_adapter_1 = require("./cloud-function.adapter");
|
|
16
|
+
const core_1 = require("@hazeljs/core");
|
|
17
|
+
describe('Serverless Decorator', () => {
|
|
18
|
+
it('should mark class as serverless', () => {
|
|
19
|
+
let TestController = class TestController {
|
|
20
|
+
};
|
|
21
|
+
TestController = __decorate([
|
|
22
|
+
(0, serverless_decorator_1.Serverless)()
|
|
23
|
+
], TestController);
|
|
24
|
+
expect((0, serverless_decorator_1.isServerless)(TestController)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('should store serverless metadata', () => {
|
|
27
|
+
const options = {
|
|
28
|
+
memory: 1024,
|
|
29
|
+
timeout: 60,
|
|
30
|
+
coldStartOptimization: true,
|
|
31
|
+
};
|
|
32
|
+
let TestController = class TestController {
|
|
33
|
+
};
|
|
34
|
+
TestController = __decorate([
|
|
35
|
+
(0, serverless_decorator_1.Serverless)(options)
|
|
36
|
+
], TestController);
|
|
37
|
+
const metadata = (0, serverless_decorator_1.getServerlessMetadata)(TestController);
|
|
38
|
+
expect(metadata).toBeDefined();
|
|
39
|
+
expect(metadata?.memory).toBe(1024);
|
|
40
|
+
expect(metadata?.timeout).toBe(60);
|
|
41
|
+
expect(metadata?.coldStartOptimization).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('should use default options', () => {
|
|
44
|
+
let TestController = class TestController {
|
|
45
|
+
};
|
|
46
|
+
TestController = __decorate([
|
|
47
|
+
(0, serverless_decorator_1.Serverless)()
|
|
48
|
+
], TestController);
|
|
49
|
+
const metadata = (0, serverless_decorator_1.getServerlessMetadata)(TestController);
|
|
50
|
+
expect(metadata?.memory).toBe(512);
|
|
51
|
+
expect(metadata?.timeout).toBe(30);
|
|
52
|
+
expect(metadata?.coldStartOptimization).toBe(true);
|
|
53
|
+
expect(metadata?.autoSplit).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('should return undefined for non-serverless class', () => {
|
|
56
|
+
class TestController {
|
|
57
|
+
}
|
|
58
|
+
expect((0, serverless_decorator_1.isServerless)(TestController)).toBe(false);
|
|
59
|
+
expect((0, serverless_decorator_1.getServerlessMetadata)(TestController)).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('ColdStartOptimizer', () => {
|
|
63
|
+
let optimizer;
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
optimizer = cold_start_optimizer_1.ColdStartOptimizer.getInstance();
|
|
66
|
+
optimizer.reset();
|
|
67
|
+
});
|
|
68
|
+
describe('warmUp', () => {
|
|
69
|
+
it('should warm up the application', async () => {
|
|
70
|
+
expect(optimizer.isWarm()).toBe(false);
|
|
71
|
+
await optimizer.warmUp();
|
|
72
|
+
expect(optimizer.isWarm()).toBe(true);
|
|
73
|
+
expect(optimizer.getWarmupTimestamp()).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
it('should not warm up twice', async () => {
|
|
76
|
+
await optimizer.warmUp();
|
|
77
|
+
const firstTimestamp = optimizer.getWarmupTimestamp();
|
|
78
|
+
await optimizer.warmUp();
|
|
79
|
+
const secondTimestamp = optimizer.getWarmupTimestamp();
|
|
80
|
+
expect(firstTimestamp).toBe(secondTimestamp);
|
|
81
|
+
});
|
|
82
|
+
it('should preload critical modules', async () => {
|
|
83
|
+
await optimizer.warmUp();
|
|
84
|
+
const preloaded = optimizer.getPreloadedModules();
|
|
85
|
+
expect(preloaded.length).toBeGreaterThan(0);
|
|
86
|
+
expect(preloaded).toContain('http');
|
|
87
|
+
expect(preloaded).toContain('crypto');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('isWarm', () => {
|
|
91
|
+
it('should return false initially', () => {
|
|
92
|
+
expect(optimizer.isWarm()).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it('should return true after warmup', async () => {
|
|
95
|
+
await optimizer.warmUp();
|
|
96
|
+
expect(optimizer.isWarm()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('getWarmupDuration', () => {
|
|
100
|
+
it('should return undefined before warmup', () => {
|
|
101
|
+
expect(optimizer.getWarmupDuration()).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('should return duration after warmup', async () => {
|
|
104
|
+
await optimizer.warmUp();
|
|
105
|
+
const duration = optimizer.getWarmupDuration();
|
|
106
|
+
expect(duration).toBeDefined();
|
|
107
|
+
expect(duration).toBeGreaterThanOrEqual(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('reset', () => {
|
|
111
|
+
it('should reset warmup state', async () => {
|
|
112
|
+
await optimizer.warmUp();
|
|
113
|
+
expect(optimizer.isWarm()).toBe(true);
|
|
114
|
+
optimizer.reset();
|
|
115
|
+
expect(optimizer.isWarm()).toBe(false);
|
|
116
|
+
expect(optimizer.getWarmupTimestamp()).toBeUndefined();
|
|
117
|
+
expect(optimizer.getPreloadedModules()).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('OptimizeColdStart Decorator', () => {
|
|
122
|
+
let optimizer;
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
optimizer = cold_start_optimizer_1.ColdStartOptimizer.getInstance();
|
|
125
|
+
optimizer.reset();
|
|
126
|
+
});
|
|
127
|
+
it('should warm up before method execution', async () => {
|
|
128
|
+
class TestClass {
|
|
129
|
+
async testMethod() {
|
|
130
|
+
return 'result';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
__decorate([
|
|
134
|
+
(0, cold_start_optimizer_1.OptimizeColdStart)(),
|
|
135
|
+
__metadata("design:type", Function),
|
|
136
|
+
__metadata("design:paramtypes", []),
|
|
137
|
+
__metadata("design:returntype", Promise)
|
|
138
|
+
], TestClass.prototype, "testMethod", null);
|
|
139
|
+
const instance = new TestClass();
|
|
140
|
+
expect(optimizer.isWarm()).toBe(false);
|
|
141
|
+
const result = await instance.testMethod();
|
|
142
|
+
expect(result).toBe('result');
|
|
143
|
+
expect(optimizer.isWarm()).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('KeepAliveHelper', () => {
|
|
147
|
+
let helper;
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
helper = new cold_start_optimizer_1.KeepAliveHelper();
|
|
150
|
+
});
|
|
151
|
+
afterEach(() => {
|
|
152
|
+
helper.stop();
|
|
153
|
+
});
|
|
154
|
+
it('should start keep-alive', () => {
|
|
155
|
+
expect(() => {
|
|
156
|
+
helper.start('http://example.com', 1000);
|
|
157
|
+
}).not.toThrow();
|
|
158
|
+
});
|
|
159
|
+
it('should stop keep-alive', () => {
|
|
160
|
+
helper.start('http://example.com', 1000);
|
|
161
|
+
expect(() => {
|
|
162
|
+
helper.stop();
|
|
163
|
+
}).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('LambdaAdapter', () => {
|
|
167
|
+
let TestModule = class TestModule {
|
|
168
|
+
};
|
|
169
|
+
TestModule = __decorate([
|
|
170
|
+
(0, core_1.HazelModule)({
|
|
171
|
+
controllers: [],
|
|
172
|
+
})
|
|
173
|
+
], TestModule);
|
|
174
|
+
let adapter;
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
adapter = new lambda_adapter_1.LambdaAdapter(TestModule);
|
|
177
|
+
});
|
|
178
|
+
it('should create Lambda adapter', () => {
|
|
179
|
+
expect(adapter).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
it('should create handler function', () => {
|
|
182
|
+
const handler = adapter.createHandler();
|
|
183
|
+
expect(handler).toBeDefined();
|
|
184
|
+
expect(typeof handler).toBe('function');
|
|
185
|
+
});
|
|
186
|
+
it('should be cold on first check', () => {
|
|
187
|
+
expect(adapter.isCold()).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('createLambdaHandler', () => {
|
|
191
|
+
let TestModule = class TestModule {
|
|
192
|
+
};
|
|
193
|
+
TestModule = __decorate([
|
|
194
|
+
(0, core_1.HazelModule)({
|
|
195
|
+
controllers: [],
|
|
196
|
+
})
|
|
197
|
+
], TestModule);
|
|
198
|
+
it('should create Lambda handler', () => {
|
|
199
|
+
const handler = (0, lambda_adapter_1.createLambdaHandler)(TestModule);
|
|
200
|
+
expect(handler).toBeDefined();
|
|
201
|
+
expect(typeof handler).toBe('function');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('CloudFunctionAdapter', () => {
|
|
205
|
+
let TestModule = class TestModule {
|
|
206
|
+
};
|
|
207
|
+
TestModule = __decorate([
|
|
208
|
+
(0, core_1.HazelModule)({
|
|
209
|
+
controllers: [],
|
|
210
|
+
})
|
|
211
|
+
], TestModule);
|
|
212
|
+
let adapter;
|
|
213
|
+
beforeEach(() => {
|
|
214
|
+
adapter = new cloud_function_adapter_1.CloudFunctionAdapter(TestModule);
|
|
215
|
+
});
|
|
216
|
+
it('should create Cloud Function adapter', () => {
|
|
217
|
+
expect(adapter).toBeDefined();
|
|
218
|
+
});
|
|
219
|
+
it('should create HTTP handler', () => {
|
|
220
|
+
const handler = adapter.createHttpHandler();
|
|
221
|
+
expect(handler).toBeDefined();
|
|
222
|
+
expect(typeof handler).toBe('function');
|
|
223
|
+
});
|
|
224
|
+
it('should create event handler', () => {
|
|
225
|
+
const handler = adapter.createEventHandler();
|
|
226
|
+
expect(handler).toBeDefined();
|
|
227
|
+
expect(typeof handler).toBe('function');
|
|
228
|
+
});
|
|
229
|
+
it('should be cold on first check', () => {
|
|
230
|
+
expect(adapter.isCold()).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
describe('createCloudFunctionHandler', () => {
|
|
234
|
+
let TestModule = class TestModule {
|
|
235
|
+
};
|
|
236
|
+
TestModule = __decorate([
|
|
237
|
+
(0, core_1.HazelModule)({
|
|
238
|
+
controllers: [],
|
|
239
|
+
})
|
|
240
|
+
], TestModule);
|
|
241
|
+
it('should create Cloud Function HTTP handler', () => {
|
|
242
|
+
const handler = (0, cloud_function_adapter_1.createCloudFunctionHandler)(TestModule);
|
|
243
|
+
expect(handler).toBeDefined();
|
|
244
|
+
expect(typeof handler).toBe('function');
|
|
245
|
+
});
|
|
246
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/serverless",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.56",
|
|
4
4
|
"description": "Serverless adapters (AWS Lambda, Google Cloud Functions) for HazelJS framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,5 +48,5 @@
|
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
50
50
|
},
|
|
51
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "c2737e90974458a8438eee623726f0a453b66b8b"
|
|
52
52
|
}
|