@mettlecast/domain-cdk-packer 0.2.60 → 0.2.62
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/dist/DomainStack.d.ts +5 -0
- package/dist/DomainStack.js +145 -67
- package/dist/__tests__/action-construct.test.d.ts +1 -0
- package/dist/__tests__/action-construct.test.js +159 -0
- package/dist/__tests__/domain-stack.test.js +315 -15
- package/dist/__tests__/grouped-lambda-factory-action-wrapper.test.d.ts +11 -0
- package/dist/__tests__/grouped-lambda-factory-action-wrapper.test.js +47 -0
- package/dist/__tests__/grouped-lambda-factory.test.d.ts +1 -0
- package/dist/__tests__/grouped-lambda-factory.test.js +146 -0
- package/dist/__tests__/lambda-factory.test.js +7 -7
- package/dist/__tests__/registry.test.js +111 -4
- package/dist/__tests__/security-assertion-aspect.test.d.ts +1 -0
- package/dist/__tests__/security-assertion-aspect.test.js +222 -0
- package/dist/aspects/index.d.ts +4 -0
- package/dist/aspects/index.js +2 -0
- package/dist/aspects/security-assertion-aspect.d.ts +161 -0
- package/dist/aspects/security-assertion-aspect.js +226 -0
- package/dist/constructs/action-construct.d.ts +16 -6
- package/dist/constructs/action-construct.js +10 -11
- package/dist/constructs/api-construct.d.ts +10 -2
- package/dist/constructs/api-construct.js +25 -5
- package/dist/grouped-lambda-factory.d.ts +69 -1
- package/dist/grouped-lambda-factory.js +98 -10
- package/dist/iam/iam-policy-builder.js +0 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/pack-domain.d.ts +2 -0
- package/dist/pack-domain.js +9 -3
- package/dist/registry.d.ts +98 -47
- package/package.json +1 -1
|
@@ -5,31 +5,61 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { DomainStack } from '../DomainStack.js';
|
|
7
7
|
const DOMAIN_ROOT = path.join(process.cwd(), '.test-domain-cdk-packer');
|
|
8
|
+
// Wave 7 Task 7.3 (#4619): removed the runtime-export probe that used to
|
|
9
|
+
// gate the action-exposure block via `describe.skipIf(...)`. The probe
|
|
10
|
+
// returned `false` whenever the workspace-resolved `@mettlecast/domain-runtime`
|
|
11
|
+
// package lagged behind the integration branch, which silently masked
|
|
12
|
+
// regressions in the route/wrapper wiring that this block exists to catch.
|
|
13
|
+
//
|
|
14
|
+
// Coverage for the adapter-selection logic itself moved to
|
|
15
|
+
// `grouped-lambda-factory.test.ts` (pure, no CDK / esbuild, no dependency
|
|
16
|
+
// on the published runtime state). This file still exercises the
|
|
17
|
+
// end-to-end CDK wiring — route creation, JWT authorizer attachment, and
|
|
18
|
+
// the "no route for internal actions" guard — so the integration-level
|
|
19
|
+
// regressions the probe used to hide are now caught by the narrow unit
|
|
20
|
+
// tests instead.
|
|
8
21
|
const minimalRegistry = {
|
|
9
22
|
schemaVersion: '1',
|
|
10
23
|
domainRoot: DOMAIN_ROOT,
|
|
11
24
|
domain: { id: 'test-domain', kind: 'domain', name: 'Test Domain', tenancy: 'none' },
|
|
12
|
-
|
|
25
|
+
// Issue #4689: the canonical HTTP endpoint surface is actions[] with
|
|
26
|
+
// exposure.type === 'api'. The minimal registry now declares its
|
|
27
|
+
// single test endpoint as an action; apis is always empty.
|
|
28
|
+
webhooks: [],
|
|
29
|
+
subscribers: [],
|
|
30
|
+
schedules: [],
|
|
31
|
+
jobs: [],
|
|
32
|
+
actions: [
|
|
13
33
|
{
|
|
14
34
|
id: 'get-users',
|
|
15
|
-
kind: '
|
|
35
|
+
kind: 'action',
|
|
16
36
|
handlerFile: 'src/handlers/get-users.ts',
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
37
|
+
backendAccess: 'domain',
|
|
38
|
+
exposure: {
|
|
39
|
+
type: 'api',
|
|
40
|
+
path: '/v1/tenants/{tenantId}/users',
|
|
41
|
+
method: 'GET',
|
|
42
|
+
auth: 'required',
|
|
43
|
+
tenancy: 'required',
|
|
44
|
+
},
|
|
45
|
+
idempotent: false,
|
|
20
46
|
},
|
|
21
47
|
],
|
|
22
|
-
webhooks: [],
|
|
23
|
-
subscribers: [],
|
|
24
|
-
schedules: [],
|
|
25
|
-
jobs: [],
|
|
26
|
-
actions: [],
|
|
27
48
|
integrations: [],
|
|
28
49
|
events: [],
|
|
29
50
|
};
|
|
30
51
|
const handlerDir = path.join(DOMAIN_ROOT, 'src', 'handlers');
|
|
31
52
|
fs.mkdirSync(handlerDir, { recursive: true });
|
|
32
|
-
|
|
53
|
+
// Match the grouped-lambda-factory's export-name conversion: kebab-case
|
|
54
|
+
// ids map to camelCase export names (e.g. `get-users` → `getUsers`).
|
|
55
|
+
const toExportName = (id) => id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
56
|
+
const handlerStub = (id, method, path, auth = 'required', securityException) => `export const ${toExportName(id)} = { id: ${JSON.stringify(id)}, backendAccess: "domain", exposure: { type: "api", path: ${JSON.stringify(path)}, method: ${JSON.stringify(method)}, auth: ${JSON.stringify(auth)}, tenancy: "required"${securityException ? `, securityException: { reason: ${JSON.stringify(securityException)} }` : ''} }, input: { parse: (x) => x }, output: { parse: (x) => x }, idempotent: false, handler: async () => ({ ok: true }) };\n`;
|
|
57
|
+
fs.writeFileSync(path.join(handlerDir, 'get-users.ts'), handlerStub('get-users', 'GET', '/v1/tenants/{tenantId}/users'));
|
|
58
|
+
fs.writeFileSync(path.join(handlerDir, 'create-user.ts'), handlerStub('create-user', 'POST', '/v1/tenants/{tenantId}/users'));
|
|
59
|
+
fs.writeFileSync(path.join(handlerDir, 'replace-user.ts'), handlerStub('replace-user', 'PUT', '/v1/tenants/{tenantId}/users/{id}'));
|
|
60
|
+
fs.writeFileSync(path.join(handlerDir, 'patch-user.ts'), handlerStub('patch-user', 'PATCH', '/v1/tenants/{tenantId}/users/{id}'));
|
|
61
|
+
fs.writeFileSync(path.join(handlerDir, 'delete-user.ts'), handlerStub('delete-user', 'DELETE', '/v1/tenants/{tenantId}/users/{id}'));
|
|
62
|
+
fs.writeFileSync(path.join(handlerDir, 'list-things.ts'), handlerStub('list-things', 'GET', '/v1/tenants/{tenantId}/things', 'none', 'public read-only listing'));
|
|
33
63
|
afterAll(() => {
|
|
34
64
|
fs.rmSync(DOMAIN_ROOT, { recursive: true, force: true });
|
|
35
65
|
});
|
|
@@ -66,14 +96,22 @@ describe('DomainStack', () => {
|
|
|
66
96
|
AuthorizerType: 'JWT',
|
|
67
97
|
});
|
|
68
98
|
});
|
|
69
|
-
it('no authorizer attached when
|
|
99
|
+
it('no authorizer attached when no JWT-protected actions are declared', () => {
|
|
70
100
|
const app = new cdk.App();
|
|
71
101
|
const registryNoAuth = {
|
|
72
102
|
...minimalRegistry,
|
|
73
|
-
|
|
103
|
+
// Replace the JWT action with an auth=none + securityException
|
|
104
|
+
// action so the authorizer is not required.
|
|
105
|
+
actions: [
|
|
74
106
|
{
|
|
75
|
-
|
|
76
|
-
|
|
107
|
+
id: 'list-things', kind: 'action', handlerFile: 'src/handlers/list-things.ts',
|
|
108
|
+
backendAccess: 'domain',
|
|
109
|
+
exposure: {
|
|
110
|
+
type: 'api', path: '/v1/tenants/{tenantId}/things', method: 'GET',
|
|
111
|
+
auth: 'none', tenancy: 'required',
|
|
112
|
+
securityException: { reason: 'public read-only listing' },
|
|
113
|
+
},
|
|
114
|
+
idempotent: false,
|
|
77
115
|
},
|
|
78
116
|
],
|
|
79
117
|
};
|
|
@@ -169,4 +207,266 @@ describe('DomainStack', () => {
|
|
|
169
207
|
template.hasOutput('DomainTenantScopedRoleArn', {});
|
|
170
208
|
});
|
|
171
209
|
});
|
|
210
|
+
/**
|
|
211
|
+
* Wave 4 Task 4.1 — fixes the route key bug where every non-GET method was
|
|
212
|
+
* silently coerced to "POST" and the JWT authorizer was never attached
|
|
213
|
+
* because the HttpJwtAuthorizer was never bound to a route.
|
|
214
|
+
*/
|
|
215
|
+
describe('route keys and JWT authorizer wiring (#4619)', () => {
|
|
216
|
+
const userPoolArn = 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz';
|
|
217
|
+
const userPoolClientId = 'test-client-id';
|
|
218
|
+
/**
|
|
219
|
+
* Build a registry whose API entries cover all HTTP method variants the
|
|
220
|
+
* route key bug previously broke. Each entry uses `authType: 'jwt'` so
|
|
221
|
+
* every method exercises the new JWT-wiring path through `addRouteToApi`.
|
|
222
|
+
*/
|
|
223
|
+
const multiMethodRegistry = {
|
|
224
|
+
...minimalRegistry,
|
|
225
|
+
actions: [
|
|
226
|
+
{
|
|
227
|
+
id: 'get-users', kind: 'action', handlerFile: 'src/handlers/get-users.ts',
|
|
228
|
+
backendAccess: 'domain',
|
|
229
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users', method: 'GET', auth: 'required', tenancy: 'required' },
|
|
230
|
+
idempotent: false,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'create-user', kind: 'action', handlerFile: 'src/handlers/create-user.ts',
|
|
234
|
+
backendAccess: 'domain',
|
|
235
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users', method: 'POST', auth: 'required', tenancy: 'required' },
|
|
236
|
+
idempotent: false,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: 'replace-user', kind: 'action', handlerFile: 'src/handlers/replace-user.ts',
|
|
240
|
+
backendAccess: 'domain',
|
|
241
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users/{id}', method: 'PUT', auth: 'required', tenancy: 'required' },
|
|
242
|
+
idempotent: false,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'patch-user', kind: 'action', handlerFile: 'src/handlers/patch-user.ts',
|
|
246
|
+
backendAccess: 'domain',
|
|
247
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users/{id}', method: 'PATCH', auth: 'required', tenancy: 'required' },
|
|
248
|
+
idempotent: false,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 'delete-user', kind: 'action', handlerFile: 'src/handlers/delete-user.ts',
|
|
252
|
+
backendAccess: 'domain',
|
|
253
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/users/{id}', method: 'DELETE', auth: 'required', tenancy: 'required' },
|
|
254
|
+
idempotent: false,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: 'list-things', kind: 'action', handlerFile: 'src/handlers/list-things.ts',
|
|
258
|
+
backendAccess: 'domain',
|
|
259
|
+
exposure: { type: 'api', path: '/v1/tenants/{tenantId}/things', method: 'GET', auth: 'none', tenancy: 'required', securityException: { reason: 'public read-only listing' } },
|
|
260
|
+
idempotent: false,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
it('uses the actual HTTP method for each route key (GET/POST/PUT/PATCH/DELETE)', () => {
|
|
265
|
+
const app = new cdk.App();
|
|
266
|
+
const stack = new DomainStack(app, 'TestDomainStackMethods', {
|
|
267
|
+
registry: multiMethodRegistry,
|
|
268
|
+
eventBusArn,
|
|
269
|
+
userPoolArn,
|
|
270
|
+
userPoolClientId,
|
|
271
|
+
});
|
|
272
|
+
const template = Template.fromStack(stack);
|
|
273
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
274
|
+
const routeKeys = Object.values(routes).map(r => r.Properties.RouteKey);
|
|
275
|
+
// Every method must round-trip — the previous implementation coerced all
|
|
276
|
+
// non-GET methods to POST, so PUT/PATCH/DELETE were broken.
|
|
277
|
+
// Issue #4689: routes now use the action's exposure.path (which
|
|
278
|
+
// already includes `/v1/tenants/{tenantId}/`) and the canonical
|
|
279
|
+
// tenant placeholder.
|
|
280
|
+
expect(routeKeys).toContain('GET /test-domain/v1/tenants/{tenantId}/users');
|
|
281
|
+
expect(routeKeys).toContain('POST /test-domain/v1/tenants/{tenantId}/users');
|
|
282
|
+
expect(routeKeys).toContain('PUT /test-domain/v1/tenants/{tenantId}/users/{id}');
|
|
283
|
+
expect(routeKeys).toContain('PATCH /test-domain/v1/tenants/{tenantId}/users/{id}');
|
|
284
|
+
expect(routeKeys).toContain('DELETE /test-domain/v1/tenants/{tenantId}/users/{id}');
|
|
285
|
+
});
|
|
286
|
+
it('sets AuthorizationType=JWT and AuthorizerId on jwt-auth routes', () => {
|
|
287
|
+
const app = new cdk.App();
|
|
288
|
+
const stack = new DomainStack(app, 'TestDomainStackRouteAuth', {
|
|
289
|
+
registry: multiMethodRegistry,
|
|
290
|
+
eventBusArn,
|
|
291
|
+
userPoolArn,
|
|
292
|
+
userPoolClientId,
|
|
293
|
+
});
|
|
294
|
+
const template = Template.fromStack(stack);
|
|
295
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
296
|
+
const jwtRoutes = Object.values(routes).filter(r => {
|
|
297
|
+
const key = r.Properties.RouteKey;
|
|
298
|
+
// Issue #4689: paths now include the tenant placeholder.
|
|
299
|
+
return key.includes('/users') && !key.includes('Health') && !key.includes('Ready');
|
|
300
|
+
});
|
|
301
|
+
// Every jwt-auth route must carry AuthorizationType and AuthorizerId.
|
|
302
|
+
expect(jwtRoutes.length).toBeGreaterThan(0);
|
|
303
|
+
for (const r of jwtRoutes) {
|
|
304
|
+
expect(r.Properties.AuthorizationType).toBe('JWT');
|
|
305
|
+
const authorizerId = r.Properties.AuthorizerId;
|
|
306
|
+
expect(authorizerId).toBeDefined();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
it('does not attach authorizer to non-jwt routes (auth=none)', () => {
|
|
310
|
+
const app = new cdk.App();
|
|
311
|
+
const stack = new DomainStack(app, 'TestDomainStackUnauthRoute', {
|
|
312
|
+
registry: multiMethodRegistry,
|
|
313
|
+
eventBusArn,
|
|
314
|
+
userPoolArn,
|
|
315
|
+
userPoolClientId,
|
|
316
|
+
});
|
|
317
|
+
const template = Template.fromStack(stack);
|
|
318
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
319
|
+
const unauthRoute = Object.values(routes).find(r => r.Properties.RouteKey === 'GET /test-domain/v1/tenants/{tenantId}/things');
|
|
320
|
+
expect(unauthRoute).toBeDefined();
|
|
321
|
+
expect(unauthRoute.Properties.AuthorizationType).toBeUndefined();
|
|
322
|
+
expect(unauthRoute.Properties.AuthorizerId).toBeUndefined();
|
|
323
|
+
});
|
|
324
|
+
it('emits a synth-time error annotation when JWT route is declared without Cognito config', () => {
|
|
325
|
+
const app = new cdk.App();
|
|
326
|
+
const stack = new DomainStack(app, 'TestDomainStackMissingPool', {
|
|
327
|
+
registry: multiMethodRegistry,
|
|
328
|
+
eventBusArn,
|
|
329
|
+
// No userPoolArn / userPoolId / userPoolClientId provided
|
|
330
|
+
});
|
|
331
|
+
// `Annotations.of(stack).errors` is not exposed; read the construct's
|
|
332
|
+
// metadata directly and filter for error-level entries.
|
|
333
|
+
const metadata = stack.node.metadata;
|
|
334
|
+
const errorMessages = metadata
|
|
335
|
+
.filter(m => m.type === 'aws:cdk:error')
|
|
336
|
+
.map(m => (typeof m.data === 'string' ? m.data : JSON.stringify(m.data)));
|
|
337
|
+
expect(errorMessages.some(a => a.includes('JWT-protected routes') && a.includes('Cognito'))).toBe(true);
|
|
338
|
+
});
|
|
339
|
+
it('does NOT emit the missing-pool error when no JWT-protected actions are present', () => {
|
|
340
|
+
const app = new cdk.App();
|
|
341
|
+
const noJwtRegistry = {
|
|
342
|
+
...minimalRegistry,
|
|
343
|
+
// Replace the JWT-protected action with an auth=none + securityException
|
|
344
|
+
// action so the JWT-needs-pool check does not fire.
|
|
345
|
+
actions: [
|
|
346
|
+
{
|
|
347
|
+
id: 'list-things', kind: 'action', handlerFile: 'src/handlers/list-things.ts',
|
|
348
|
+
backendAccess: 'domain',
|
|
349
|
+
exposure: {
|
|
350
|
+
type: 'api', path: '/v1/tenants/{tenantId}/things', method: 'GET',
|
|
351
|
+
auth: 'none', tenancy: 'required',
|
|
352
|
+
securityException: { reason: 'public read-only listing' },
|
|
353
|
+
},
|
|
354
|
+
idempotent: false,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
const stack = new DomainStack(app, 'TestDomainStackNoJwtNeeded', {
|
|
359
|
+
registry: noJwtRegistry,
|
|
360
|
+
eventBusArn,
|
|
361
|
+
});
|
|
362
|
+
const metadata = stack.node.metadata;
|
|
363
|
+
const errorMessages = metadata
|
|
364
|
+
.filter(m => m.type === 'aws:cdk:error')
|
|
365
|
+
.map(m => (typeof m.data === 'string' ? m.data : JSON.stringify(m.data)));
|
|
366
|
+
expect(errorMessages.some(a => a.includes('JWT-protected routes'))).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
/**
|
|
370
|
+
* Wave 4 Task 4.1 — generate API Gateway routes for actions whose
|
|
371
|
+
* `exposure.type === 'api'` and skip `exposure.type === 'internal'`.
|
|
372
|
+
*
|
|
373
|
+
* Wave 7 Task 7.3 (#4619) removed the runtime-export probe and the
|
|
374
|
+
* `describe.skipIf(...)` wrapper. The route-level assertions still need
|
|
375
|
+
* a full CDK synth so the L1 `CfnRoute` resources can be inspected, but
|
|
376
|
+
* adapter-selection coverage (the part most likely to regress when the
|
|
377
|
+
* runtime barrel changes) lives in `grouped-lambda-factory.test.ts`
|
|
378
|
+
* where it runs as a pure unit test in milliseconds. If the runtime
|
|
379
|
+
* barrel ever drops an export we expect to see this block fail with a
|
|
380
|
+
* bundling error rather than be silently skipped.
|
|
381
|
+
*/
|
|
382
|
+
describe('action exposure routing (#4619)', () => {
|
|
383
|
+
const actionHandlerDir = path.join(DOMAIN_ROOT, 'src', 'handlers');
|
|
384
|
+
fs.mkdirSync(actionHandlerDir, { recursive: true });
|
|
385
|
+
fs.writeFileSync(path.join(actionHandlerDir, 'list-invoices.ts'), 'export const listInvoices = { id: "list-invoices", backendAccess: "domain", exposure: { type: "api", path: "/v1/tenants/{tenantId}/invoices", method: "GET", auth: "required", tenancy: "required" }, input: { parse: (x: unknown) => x }, output: { parse: (x: unknown) => x }, idempotent: false, handler: async () => ({ ok: true }) };\n');
|
|
386
|
+
fs.writeFileSync(path.join(actionHandlerDir, 'charge-card.ts'), 'export const chargeCard = { id: "charge-card", backendAccess: "domain", exposure: { type: "api", path: "/v1/tenants/{tenantId}/charge", method: "POST", auth: "required", tenancy: "required" }, input: { parse: (x: unknown) => x }, output: { parse: (x: unknown) => x }, idempotent: true, handler: async () => ({ ok: true }) };\n');
|
|
387
|
+
fs.writeFileSync(path.join(actionHandlerDir, 'internal-helper.ts'), 'export const internalHelper = { id: "internal-helper", backendAccess: "private", exposure: { type: "internal" }, input: { parse: (x: unknown) => x }, output: { parse: (x: unknown) => x }, idempotent: false, handler: async () => ({ ok: true }) };\n');
|
|
388
|
+
const registryWithActions = {
|
|
389
|
+
...minimalRegistry,
|
|
390
|
+
actions: [
|
|
391
|
+
{
|
|
392
|
+
id: 'list-invoices',
|
|
393
|
+
kind: 'action',
|
|
394
|
+
handlerFile: 'src/handlers/list-invoices.ts',
|
|
395
|
+
backendAccess: 'domain',
|
|
396
|
+
exposure: {
|
|
397
|
+
type: 'api',
|
|
398
|
+
path: '/v1/tenants/{tenantId}/invoices',
|
|
399
|
+
method: 'GET',
|
|
400
|
+
auth: 'required',
|
|
401
|
+
tenancy: 'required',
|
|
402
|
+
},
|
|
403
|
+
idempotent: false,
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: 'charge-card',
|
|
407
|
+
kind: 'action',
|
|
408
|
+
handlerFile: 'src/handlers/charge-card.ts',
|
|
409
|
+
backendAccess: 'domain',
|
|
410
|
+
exposure: {
|
|
411
|
+
type: 'api',
|
|
412
|
+
path: '/v1/tenants/{tenantId}/charge',
|
|
413
|
+
method: 'POST',
|
|
414
|
+
auth: 'required',
|
|
415
|
+
tenancy: 'required',
|
|
416
|
+
},
|
|
417
|
+
idempotent: true,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'internal-helper',
|
|
421
|
+
kind: 'action',
|
|
422
|
+
handlerFile: 'src/handlers/internal-helper.ts',
|
|
423
|
+
backendAccess: 'private',
|
|
424
|
+
exposure: { type: 'internal' },
|
|
425
|
+
idempotent: false,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
it('synthesises routes for API-exposed actions', () => {
|
|
430
|
+
const app = new cdk.App();
|
|
431
|
+
const stack = new DomainStack(app, 'TestDomainStackActionRoutes', {
|
|
432
|
+
registry: registryWithActions,
|
|
433
|
+
eventBusArn,
|
|
434
|
+
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
435
|
+
userPoolClientId: 'test-client-id',
|
|
436
|
+
});
|
|
437
|
+
const template = Template.fromStack(stack);
|
|
438
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
439
|
+
const routeKeys = Object.values(routes).map(r => r.Properties.RouteKey);
|
|
440
|
+
expect(routeKeys).toContain('GET /test-domain/v1/tenants/{tenantId}/invoices');
|
|
441
|
+
expect(routeKeys).toContain('POST /test-domain/v1/tenants/{tenantId}/charge');
|
|
442
|
+
});
|
|
443
|
+
it('does NOT create a route for internal exposure actions', () => {
|
|
444
|
+
const app = new cdk.App();
|
|
445
|
+
const stack = new DomainStack(app, 'TestDomainStackInternalOnly', {
|
|
446
|
+
registry: registryWithActions,
|
|
447
|
+
eventBusArn,
|
|
448
|
+
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
449
|
+
userPoolClientId: 'test-client-id',
|
|
450
|
+
});
|
|
451
|
+
const template = Template.fromStack(stack);
|
|
452
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
453
|
+
const routeKeys = Object.values(routes).map(r => r.Properties.RouteKey);
|
|
454
|
+
expect(routeKeys.some(k => k.includes('internal-helper'))).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
it('attaches JWT authorizer to API-exposed action routes when auth=required', () => {
|
|
457
|
+
const app = new cdk.App();
|
|
458
|
+
const stack = new DomainStack(app, 'TestDomainStackActionJwt', {
|
|
459
|
+
registry: registryWithActions,
|
|
460
|
+
eventBusArn,
|
|
461
|
+
userPoolArn: 'arn:aws:cognito-idp:eu-north-1:123456789012:userpool/eu-north-1_abc123xyz',
|
|
462
|
+
userPoolClientId: 'test-client-id',
|
|
463
|
+
});
|
|
464
|
+
const template = Template.fromStack(stack);
|
|
465
|
+
const routes = template.findResources('AWS::ApiGatewayV2::Route');
|
|
466
|
+
const actionRoute = Object.values(routes).find(r => r.Properties.RouteKey === 'POST /test-domain/v1/tenants/{tenantId}/charge');
|
|
467
|
+
expect(actionRoute).toBeDefined();
|
|
468
|
+
expect(actionRoute.Properties.AuthorizationType).toBe('JWT');
|
|
469
|
+
expect(actionRoute.Properties.AuthorizerId).toBeDefined();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
172
472
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 7 Task 7.1 (#4619) — generated dedicated Lambda wrapper tests.
|
|
3
|
+
*
|
|
4
|
+
* The dedicated-mode bundler writes an entry module that wires a domain action
|
|
5
|
+
* export into a runtime adapter. For action adapters the wiring is non-trivial:
|
|
6
|
+
* `createActionLambdaHandler` takes `(registry, options)` and
|
|
7
|
+
* `createExposedActionApiHandler` takes `(action, options)`. These assertions
|
|
8
|
+
* use the pure entry-content helper so they do not race with other tests that
|
|
9
|
+
* also write `.tib-domain-entries` artifacts.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wave 7 Task 7.1 (#4619) — generated dedicated Lambda wrapper tests.
|
|
3
|
+
*
|
|
4
|
+
* The dedicated-mode bundler writes an entry module that wires a domain action
|
|
5
|
+
* export into a runtime adapter. For action adapters the wiring is non-trivial:
|
|
6
|
+
* `createActionLambdaHandler` takes `(registry, options)` and
|
|
7
|
+
* `createExposedActionApiHandler` takes `(action, options)`. These assertions
|
|
8
|
+
* use the pure entry-content helper so they do not race with other tests that
|
|
9
|
+
* also write `.tib-domain-entries` artifacts.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { buildDedicatedEntryContent } from '../grouped-lambda-factory.js';
|
|
13
|
+
const DOMAIN_ID = 'billing';
|
|
14
|
+
function render(entry, primitiveType = 'action') {
|
|
15
|
+
return buildDedicatedEntryContent('./handler.js', entry, primitiveType, DOMAIN_ID);
|
|
16
|
+
}
|
|
17
|
+
describe('grouped-lambda-factory — generated action wrapper (Wave 7 Task 7.1)', () => {
|
|
18
|
+
it('emits a createActionLambdaHandler(registry, options) wrapper for internal actions', () => {
|
|
19
|
+
const content = render({ id: 'internal-helper', handlerFile: 'src/actions/internal-helper.ts' });
|
|
20
|
+
expect(content).toMatch(/import\s*\{\s*createActionLambdaHandler\s*\}\s*from\s*['"]@mettlecast\/domain-runtime['"]/);
|
|
21
|
+
expect(content).toMatch(/const\s+registry\s*=\s*\{\s*"billing"\s*:\s*\{\s*"internal-helper"\s*:\s*internalHelper\s*\}\s*\}/);
|
|
22
|
+
expect(content).toMatch(/export\s+const\s+handler\s*=\s*createActionLambdaHandler\(\s*registry\s*,\s*\{/);
|
|
23
|
+
expect(content).toMatch(/domainId:\s*"billing"/);
|
|
24
|
+
expect(content).toMatch(/databaseUrl:\s*process\.env\.DATABASE_URL/);
|
|
25
|
+
expect(content).toMatch(/eventBusName:\s*process\.env\.EVENT_BUS_NAME/);
|
|
26
|
+
expect(content).toMatch(/idempotencyTableName:\s*process\.env\.IDEMPOTENCY_TABLE/);
|
|
27
|
+
});
|
|
28
|
+
it('emits a createExposedActionApiHandler(action, options) wrapper for API-exposed actions', () => {
|
|
29
|
+
const content = render({
|
|
30
|
+
id: 'public-charge',
|
|
31
|
+
handlerFile: 'src/actions/public-charge.ts',
|
|
32
|
+
adapter: 'createExposedActionApiHandler',
|
|
33
|
+
});
|
|
34
|
+
expect(content).toMatch(/import\s*\{\s*createExposedActionApiHandler\s*\}\s*from\s*['"]@mettlecast\/domain-runtime['"]/);
|
|
35
|
+
expect(content).toMatch(/const\s+actionRegistry\s*=\s*\{\s*"billing"\s*:\s*\{\s*"public-charge"\s*:\s*publicCharge\s*\}\s*\}/);
|
|
36
|
+
expect(content).toMatch(/export\s+const\s+handler\s*=\s*createExposedActionApiHandler\(\s*publicCharge\s*,\s*\{/);
|
|
37
|
+
expect(content).toMatch(/definingDomain:\s*"billing"/);
|
|
38
|
+
expect(content).toMatch(/domainId:\s*"billing"/);
|
|
39
|
+
expect(content).toMatch(/actionRegistry,/);
|
|
40
|
+
expect(content).toMatch(/callerDomainId:\s*"billing"/);
|
|
41
|
+
});
|
|
42
|
+
it('does NOT emit a bare `${adapter}(${exportName})` shape for any action adapter', () => {
|
|
43
|
+
const content = render({ id: 'another-internal', handlerFile: 'src/actions/another-internal.ts' });
|
|
44
|
+
expect(content).not.toMatch(/createActionLambdaHandler\(\s*anotherInternal\s*\)/);
|
|
45
|
+
expect(content).not.toMatch(/createExposedActionApiHandler\(\s*anotherInternal\s*\)/);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildDedicatedEntryContent, ADAPTER_BY_PRIMITIVE, EXPOSED_ACTION_ADAPTER, } from '../grouped-lambda-factory.js';
|
|
3
|
+
/**
|
|
4
|
+
* Wave 7 Task 7.3 (#4619) — narrow, pure tests for the dedicated-mode
|
|
5
|
+
* Lambda entry-content generator.
|
|
6
|
+
*
|
|
7
|
+
* These tests pin the adapter-selection logic added in Wave 4 Task 4.1
|
|
8
|
+
* (API-exposed actions wrap with `createExposedActionApiHandler` while
|
|
9
|
+
* internal actions keep `createActionLambdaHandler`) WITHOUT spinning up
|
|
10
|
+
* CDK or esbuild. The previous version of `domain-stack.test.ts` skipped
|
|
11
|
+
* these assertions via a runtime-export probe because the installed
|
|
12
|
+
* `@mettlecast/domain-runtime` package sometimes lags behind the
|
|
13
|
+
* integration branch. Testing the source code we generate — not the
|
|
14
|
+
* downstream package state — removes that skip and the regression-masking
|
|
15
|
+
* risk that came with it.
|
|
16
|
+
*
|
|
17
|
+
* End-to-end route-creation / authorizer-attachment behaviour is still
|
|
18
|
+
* covered by `domain-stack.test.ts` (the action-exposure block at the
|
|
19
|
+
* bottom of that file), but the heavy assertions about WHICH adapter is
|
|
20
|
+
* wired into WHICH entry now live here where they run in milliseconds.
|
|
21
|
+
*
|
|
22
|
+
* Compatibility note (Task 7.1 overlap, #4619): the test surface here
|
|
23
|
+
* deliberately targets only the parts of `buildDedicatedEntryContent`
|
|
24
|
+
* that are stable across the Wave 7.1 wrapper fixes (which change the
|
|
25
|
+
* action-adapter call shape, not the adapter selection logic). When
|
|
26
|
+
* Task 7.1 lands, this file can grow assertions for the new wrappers
|
|
27
|
+
* without rewriting the existing ones.
|
|
28
|
+
*/
|
|
29
|
+
describe('buildDedicatedEntryContent (Wave 7 Task 7.3, #4619)', () => {
|
|
30
|
+
const DOMAIN_ID = 'billing';
|
|
31
|
+
const entry = (id, adapter) => ({
|
|
32
|
+
id,
|
|
33
|
+
handlerFile: `src/handlers/${id}.ts`,
|
|
34
|
+
...(adapter !== undefined ? { adapter } : {}),
|
|
35
|
+
});
|
|
36
|
+
describe('adapter selection by primitive type (defaults)', () => {
|
|
37
|
+
const cases = [
|
|
38
|
+
{ primitive: 'subscriber', expectedAdapter: 'createSubscriberLambdaHandler' },
|
|
39
|
+
{ primitive: 'job', expectedAdapter: 'createJobLambdaHandler' },
|
|
40
|
+
{ primitive: 'webhook', expectedAdapter: 'createWebhookLambdaHandler' },
|
|
41
|
+
{ primitive: 'action', expectedAdapter: 'createActionLambdaHandler' },
|
|
42
|
+
];
|
|
43
|
+
for (const { primitive, expectedAdapter } of cases) {
|
|
44
|
+
it(`imports the default adapter for primitive type "${primitive}"`, () => {
|
|
45
|
+
const content = buildDedicatedEntryContent('./handler.js', entry('list-things'), primitive, DOMAIN_ID);
|
|
46
|
+
expect(content).toContain(`import { ${expectedAdapter} } from '@mettlecast/domain-runtime';`);
|
|
47
|
+
if (primitive === 'action') {
|
|
48
|
+
expect(content).toContain(`const registry = { "${DOMAIN_ID}": { "list-things": listThings } };`);
|
|
49
|
+
expect(content).toContain('export const handler = createActionLambdaHandler(registry, {');
|
|
50
|
+
expect(content).toContain(`domainId: "${DOMAIN_ID}"`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
expect(content).toContain(`export const handler = ${expectedAdapter}(listThings);`);
|
|
54
|
+
}
|
|
55
|
+
// Adapter map should agree with what is generated
|
|
56
|
+
expect(ADAPTER_BY_PRIMITIVE[primitive]).toBe(expectedAdapter);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
it('schedule has no entry in ADAPTER_BY_PRIMITIVE (uses hydrateCtx envelope instead)', () => {
|
|
60
|
+
// Schedules intentionally fall through to the inline `hydrateCtx`
|
|
61
|
+
// wrapper path (see buildDedicatedEntryContent's schedule branch).
|
|
62
|
+
// Pinning that here guards against future refactors silently
|
|
63
|
+
// promoting schedules into the adapter-map path.
|
|
64
|
+
const keys = Object.keys(ADAPTER_BY_PRIMITIVE);
|
|
65
|
+
expect(keys).not.toContain('schedule');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('action adapter override (Wave 4 Task 4.1, #4619)', () => {
|
|
69
|
+
it('uses createExposedActionApiHandler when entry.adapter is the exposed-action adapter', () => {
|
|
70
|
+
const apiExposed = entry('list-invoices', EXPOSED_ACTION_ADAPTER);
|
|
71
|
+
const content = buildDedicatedEntryContent('./handler.js', apiExposed, 'action', DOMAIN_ID);
|
|
72
|
+
expect(content).toContain(`import { ${EXPOSED_ACTION_ADAPTER} } from '@mettlecast/domain-runtime';`);
|
|
73
|
+
expect(content).toContain(`const actionRegistry = { "${DOMAIN_ID}": { "list-invoices": listInvoices } };`);
|
|
74
|
+
expect(content).toContain(`export const handler = ${EXPOSED_ACTION_ADAPTER}(listInvoices, {`);
|
|
75
|
+
expect(content).toContain(`definingDomain: "${DOMAIN_ID}"`);
|
|
76
|
+
expect(content).toContain('actionRegistry,');
|
|
77
|
+
// Must NOT also import the envelope-based adapter — that would be
|
|
78
|
+
// dead code and risks accidental misuse if the adapter selection ever
|
|
79
|
+
// regresses.
|
|
80
|
+
expect(content).not.toContain('createActionLambdaHandler');
|
|
81
|
+
});
|
|
82
|
+
it('uses createActionLambdaHandler when no adapter override is supplied (internal action)', () => {
|
|
83
|
+
const internal = entry('internal-helper');
|
|
84
|
+
const content = buildDedicatedEntryContent('./handler.js', internal, 'action', DOMAIN_ID);
|
|
85
|
+
expect(content).toContain(`import { createActionLambdaHandler } from '@mettlecast/domain-runtime';`);
|
|
86
|
+
expect(content).toContain(`const registry = { "${DOMAIN_ID}": { "internal-helper": internalHelper } };`);
|
|
87
|
+
expect(content).toContain('export const handler = createActionLambdaHandler(registry, {');
|
|
88
|
+
expect(content).toContain(`domainId: "${DOMAIN_ID}"`);
|
|
89
|
+
// Must NOT use the exposed adapter for internal actions.
|
|
90
|
+
expect(content).not.toContain(EXPOSED_ACTION_ADAPTER);
|
|
91
|
+
});
|
|
92
|
+
it('per-entry adapter override takes precedence over the primitive-type default', () => {
|
|
93
|
+
// Even when an explicit adapter is supplied, the override wins.
|
|
94
|
+
const overridden = entry('charge-card', EXPOSED_ACTION_ADAPTER);
|
|
95
|
+
const content = buildDedicatedEntryContent('./handler.js', overridden, 'webhook', DOMAIN_ID);
|
|
96
|
+
expect(content).toContain(`import { ${EXPOSED_ACTION_ADAPTER} } from '@mettlecast/domain-runtime';`);
|
|
97
|
+
expect(content).not.toContain('createWebhookLambdaHandler');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('schedule envelope', () => {
|
|
101
|
+
it('uses hydrateCtx (no per-primitive adapter) for schedules', () => {
|
|
102
|
+
const schedule = entry('nightly-rollup');
|
|
103
|
+
const content = buildDedicatedEntryContent('./handler.js', schedule, 'schedule', DOMAIN_ID);
|
|
104
|
+
expect(content).toContain(`import { hydrateCtx } from '@mettlecast/domain-runtime';`);
|
|
105
|
+
expect(content).toContain('hydrateCtx(event, {');
|
|
106
|
+
expect(content).toContain('databaseUrl: process.env.DATABASE_URL');
|
|
107
|
+
expect(content).toContain('eventBusName: process.env.EVENT_BUS_NAME');
|
|
108
|
+
expect(content).toContain('try { await ctx.db.release(); } catch {}');
|
|
109
|
+
// Schedules should NOT use any of the Lambda-handler adapter factories.
|
|
110
|
+
expect(content).not.toMatch(/import \{ \w+LambdaHandler \}/);
|
|
111
|
+
});
|
|
112
|
+
it('does NOT honour entry.adapter for schedules — hydrateCtx is the only path', () => {
|
|
113
|
+
const schedule = entry('nightly-rollup', EXPOSED_ACTION_ADAPTER);
|
|
114
|
+
const content = buildDedicatedEntryContent('./handler.js', schedule, 'schedule', DOMAIN_ID);
|
|
115
|
+
expect(content).toContain(`import { hydrateCtx } from '@mettlecast/domain-runtime';`);
|
|
116
|
+
expect(content).not.toContain(EXPOSED_ACTION_ADAPTER);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('handler import path wiring', () => {
|
|
120
|
+
it('uses the caller-supplied relative import path verbatim', () => {
|
|
121
|
+
const e = entry('list-things');
|
|
122
|
+
const content = buildDedicatedEntryContent('../../../../src/handlers/list-things.ts', e, 'webhook', DOMAIN_ID);
|
|
123
|
+
expect(content).toContain(`import { listThings } from '../../../../src/handlers/list-things.ts';`);
|
|
124
|
+
});
|
|
125
|
+
it('converts kebab-case handler IDs to camelCase export names', () => {
|
|
126
|
+
const e = entry('list-invoices-for-tenant');
|
|
127
|
+
const content = buildDedicatedEntryContent('./handler.js', e, 'action', DOMAIN_ID);
|
|
128
|
+
expect(content).toMatch(/import \{ listInvoicesForTenant \} from/);
|
|
129
|
+
expect(content).toContain(`"list-invoices-for-tenant": listInvoicesForTenant`);
|
|
130
|
+
expect(content).toContain('createActionLambdaHandler(registry, {');
|
|
131
|
+
});
|
|
132
|
+
it('emits a single export const handler per entry', () => {
|
|
133
|
+
const e = entry('list-things');
|
|
134
|
+
const content = buildDedicatedEntryContent('./handler.js', e, 'webhook', DOMAIN_ID);
|
|
135
|
+
const matches = content.match(/^export const handler\b/gm) ?? [];
|
|
136
|
+
expect(matches.length).toBe(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('EXPOSED_ACTION_ADAPTER constant', () => {
|
|
140
|
+
it('matches the Wave 3 / Wave 4 exported name', () => {
|
|
141
|
+
// Pinning the literal value guards against silent rename during future
|
|
142
|
+
// refactors of the runtime barrel.
|
|
143
|
+
expect(EXPOSED_ACTION_ADAPTER).toBe('createExposedActionApiHandler');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -5,12 +5,12 @@ import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { LambdaFactory } from '../lambda-factory.js';
|
|
8
|
-
const DOMAIN_ROOT = '
|
|
8
|
+
const DOMAIN_ROOT = path.join(process.cwd(), '.tmp', 'test-domain-cdk-packer');
|
|
9
9
|
const minimalRegistry = {
|
|
10
10
|
schemaVersion: '1',
|
|
11
11
|
domainRoot: DOMAIN_ROOT,
|
|
12
12
|
domain: { id: 'test-domain', kind: 'domain', name: 'Test Domain', tenancy: 'none' },
|
|
13
|
-
|
|
13
|
+
webhooks: [], subscribers: [], schedules: [], jobs: [], actions: [], integrations: [], events: [],
|
|
14
14
|
};
|
|
15
15
|
beforeAll(() => {
|
|
16
16
|
const handlerDir = path.join(DOMAIN_ROOT, 'src');
|
|
@@ -26,7 +26,7 @@ describe('LambdaFactory', () => {
|
|
|
26
26
|
const stack = new cdk.Stack(app, 'TestStack');
|
|
27
27
|
const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
|
|
28
28
|
expect(() => {
|
|
29
|
-
factory.createFunction({ id: 'test-
|
|
29
|
+
factory.createFunction({ id: 'test-action', kind: 'action', handlerFile: 'src/api.ts' });
|
|
30
30
|
}).not.toThrow();
|
|
31
31
|
});
|
|
32
32
|
describe('bundling configuration', () => {
|
|
@@ -34,7 +34,7 @@ describe('LambdaFactory', () => {
|
|
|
34
34
|
const app = new cdk.App();
|
|
35
35
|
const stack = new cdk.Stack(app, 'TestStackArch');
|
|
36
36
|
const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
|
|
37
|
-
factory.createFunction({ id: 'test-
|
|
37
|
+
factory.createFunction({ id: 'test-action', kind: 'action', handlerFile: 'src/api.ts' });
|
|
38
38
|
const template = Template.fromStack(stack);
|
|
39
39
|
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
40
40
|
Architectures: ['arm64'],
|
|
@@ -44,7 +44,7 @@ describe('LambdaFactory', () => {
|
|
|
44
44
|
const app = new cdk.App();
|
|
45
45
|
const stack = new cdk.Stack(app, 'TestStackRuntime');
|
|
46
46
|
const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
|
|
47
|
-
factory.createFunction({ id: 'test-
|
|
47
|
+
factory.createFunction({ id: 'test-action', kind: 'action', handlerFile: 'src/api.ts' });
|
|
48
48
|
const template = Template.fromStack(stack);
|
|
49
49
|
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
50
50
|
Runtime: 'nodejs22.x',
|
|
@@ -54,7 +54,7 @@ describe('LambdaFactory', () => {
|
|
|
54
54
|
const app = new cdk.App();
|
|
55
55
|
const stack = new cdk.Stack(app, 'TestStackSourceMaps');
|
|
56
56
|
const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
|
|
57
|
-
factory.createFunction({ id: 'test-
|
|
57
|
+
factory.createFunction({ id: 'test-action', kind: 'action', handlerFile: 'src/api.ts' });
|
|
58
58
|
const template = Template.fromStack(stack);
|
|
59
59
|
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
60
60
|
Environment: {
|
|
@@ -68,7 +68,7 @@ describe('LambdaFactory', () => {
|
|
|
68
68
|
const app = new cdk.App();
|
|
69
69
|
const stack = new cdk.Stack(app, 'TestStackDefaults');
|
|
70
70
|
const factory = new LambdaFactory({ scope: stack, registry: minimalRegistry, domainRoot: minimalRegistry.domainRoot });
|
|
71
|
-
const fn = factory.createFunction({ id: 'test-
|
|
71
|
+
const fn = factory.createFunction({ id: 'test-action', kind: 'action', handlerFile: 'src/api.ts' });
|
|
72
72
|
expect(fn).toBeInstanceOf(lambdaNode.NodejsFunction);
|
|
73
73
|
expect(fn.runtime?.name).toContain('nodejs22');
|
|
74
74
|
});
|