serverless-event-orchestrator 2.2.0 → 2.3.0
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/LICENSE +21 -21
- package/README.md +489 -489
- package/dist/dispatcher.d.ts +6 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/dispatcher.js +31 -7
- package/dist/dispatcher.js.map +1 -1
- package/jest.config.js +32 -32
- package/package.json +82 -81
- package/src/dispatcher.ts +586 -558
- package/src/http/body-parser.ts +60 -60
- package/src/http/cors.ts +76 -76
- package/src/http/index.ts +3 -3
- package/src/http/response.ts +209 -209
- package/src/identity/extractor.ts +207 -207
- package/src/identity/index.ts +2 -2
- package/src/identity/jwt-verifier.ts +41 -41
- package/src/index.ts +128 -128
- package/src/middleware/crm-guard.ts +51 -51
- package/src/middleware/index.ts +3 -3
- package/src/middleware/init-tenant-context.ts +59 -59
- package/src/middleware/tenant-guard.ts +54 -54
- package/src/tenant/TenantContext.ts +115 -115
- package/src/tenant/helpers.ts +112 -112
- package/src/tenant/index.ts +21 -21
- package/src/tenant/types.ts +101 -101
- package/src/types/event-type.enum.ts +21 -21
- package/src/types/index.ts +2 -2
- package/src/types/routes.ts +218 -218
- package/src/utils/headers.ts +72 -72
- package/src/utils/index.ts +2 -2
- package/src/utils/path-matcher.ts +84 -84
- package/tests/cors.test.ts +133 -133
- package/tests/dispatcher.test.ts +795 -715
- package/tests/headers.test.ts +99 -99
- package/tests/identity.test.ts +301 -301
- package/tests/middleware/crm-guard.test.ts +69 -69
- package/tests/middleware/init-tenant-context.test.ts +147 -147
- package/tests/middleware/tenant-guard.test.ts +100 -100
- package/tests/path-matcher.test.ts +102 -102
- package/tests/response.test.ts +155 -155
- package/tests/tenant/TenantContext.test.ts +134 -134
- package/tests/tenant/helpers.test.ts +187 -187
- package/tsconfig.json +24 -24
package/tests/dispatcher.test.ts
CHANGED
|
@@ -1,715 +1,795 @@
|
|
|
1
|
-
import { dispatchEvent, detectEventType } from '../src/dispatcher';
|
|
2
|
-
import { EventType, RouteSegment } from '../src/types/event-type.enum';
|
|
3
|
-
import { SegmentedHttpRouter, DispatchRoutes, NormalizedEvent } from '../src/types/routes';
|
|
4
|
-
|
|
5
|
-
describe('detectEventType', () => {
|
|
6
|
-
it('should detect EventBridge events', () => {
|
|
7
|
-
const event = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
expect(detectEventType(event)).toBe(EventType.
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should
|
|
22
|
-
const event = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const event = {
|
|
199
|
-
requestContext: { requestId: '123' },
|
|
200
|
-
httpMethod: 'GET',
|
|
201
|
-
resource: '/
|
|
202
|
-
path: '/
|
|
203
|
-
headers: {},
|
|
204
|
-
body: null
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
await dispatchEvent(event, routes
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
expect(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
expect(mockHandler).
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
},
|
|
669
|
-
body: null
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
await dispatchEvent(event, routes
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
})
|
|
1
|
+
import { dispatchEvent, detectEventType } from '../src/dispatcher';
|
|
2
|
+
import { EventType, RouteSegment } from '../src/types/event-type.enum';
|
|
3
|
+
import { SegmentedHttpRouter, DispatchRoutes, NormalizedEvent } from '../src/types/routes';
|
|
4
|
+
|
|
5
|
+
describe('detectEventType', () => {
|
|
6
|
+
it('should detect EventBridge events by structural shape (any source)', () => {
|
|
7
|
+
const event = {
|
|
8
|
+
version: '0',
|
|
9
|
+
id: 'evt-12345',
|
|
10
|
+
source: 'envivienda.domain',
|
|
11
|
+
'detail-type': 'agency.member.requested',
|
|
12
|
+
detail: { foo: 'bar' },
|
|
13
|
+
account: '123',
|
|
14
|
+
region: 'us-east-1',
|
|
15
|
+
time: '2026-05-05T05:00:00Z',
|
|
16
|
+
resources: [],
|
|
17
|
+
};
|
|
18
|
+
expect(detectEventType(event)).toBe(EventType.EventBridge);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should not confuse Scheduled events with EventBridge events', () => {
|
|
22
|
+
const event = {
|
|
23
|
+
version: '0',
|
|
24
|
+
id: 'evt-12345',
|
|
25
|
+
source: 'aws.events',
|
|
26
|
+
'detail-type': 'Scheduled Event',
|
|
27
|
+
detail: {},
|
|
28
|
+
};
|
|
29
|
+
expect(detectEventType(event)).toBe(EventType.Scheduled);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should NOT detect plain objects as EventBridge', () => {
|
|
33
|
+
expect(detectEventType({ source: 'foo', detail: {} })).toBe(EventType.Unknown);
|
|
34
|
+
expect(detectEventType({ version: '0', id: '1' })).toBe(EventType.Unknown);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect API Gateway events', () => {
|
|
38
|
+
const event = { requestContext: { requestId: '123' }, httpMethod: 'GET', path: '/test' };
|
|
39
|
+
expect(detectEventType(event)).toBe(EventType.ApiGateway);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should detect Lambda invocation events', () => {
|
|
43
|
+
const event = { awsRequestId: '1234-5678' };
|
|
44
|
+
expect(detectEventType(event)).toBe(EventType.Lambda);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should detect SQS events', () => {
|
|
48
|
+
const event = {
|
|
49
|
+
Records: [
|
|
50
|
+
{ eventSource: 'aws:sqs', body: '{}', eventSourceARN: 'arn:aws:sqs:us-east-1:123:my-queue' }
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
expect(detectEventType(event)).toBe(EventType.Sqs);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should return Unknown for unrecognized events', () => {
|
|
57
|
+
const event = { foo: 'bar' };
|
|
58
|
+
expect(detectEventType(event)).toBe(EventType.Unknown);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('dispatchEvent - API Gateway', () => {
|
|
63
|
+
const mockHandler = jest.fn().mockResolvedValue({ statusCode: 200, body: '{}' });
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
mockHandler.mockClear();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should dispatch to flat HTTP router', async () => {
|
|
70
|
+
const routes: DispatchRoutes = {
|
|
71
|
+
apigateway: {
|
|
72
|
+
get: {
|
|
73
|
+
'/users': { handler: mockHandler }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const event = {
|
|
79
|
+
requestContext: { requestId: '123' },
|
|
80
|
+
httpMethod: 'GET',
|
|
81
|
+
resource: '/users',
|
|
82
|
+
path: '/users',
|
|
83
|
+
headers: {},
|
|
84
|
+
queryStringParameters: null,
|
|
85
|
+
body: null
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await dispatchEvent(event, routes);
|
|
89
|
+
|
|
90
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
eventType: EventType.ApiGateway,
|
|
94
|
+
context: expect.objectContaining({
|
|
95
|
+
segment: RouteSegment.Public
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should dispatch to segmented router', async () => {
|
|
102
|
+
const routes: DispatchRoutes = {
|
|
103
|
+
apigateway: {
|
|
104
|
+
public: {
|
|
105
|
+
get: { '/health': { handler: mockHandler } }
|
|
106
|
+
},
|
|
107
|
+
private: {
|
|
108
|
+
get: { '/profile': { handler: mockHandler } }
|
|
109
|
+
}
|
|
110
|
+
} as SegmentedHttpRouter
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const event = {
|
|
114
|
+
requestContext: { requestId: '123' },
|
|
115
|
+
httpMethod: 'GET',
|
|
116
|
+
resource: '/profile',
|
|
117
|
+
path: '/profile',
|
|
118
|
+
headers: {},
|
|
119
|
+
body: null
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await dispatchEvent(event, routes);
|
|
123
|
+
|
|
124
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
context: expect.objectContaining({
|
|
127
|
+
segment: RouteSegment.Private
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should match path parameters', async () => {
|
|
134
|
+
const routes: DispatchRoutes = {
|
|
135
|
+
apigateway: {
|
|
136
|
+
get: {
|
|
137
|
+
'/users/{id}': { handler: mockHandler }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const event = {
|
|
143
|
+
requestContext: { requestId: '123' },
|
|
144
|
+
httpMethod: 'GET',
|
|
145
|
+
resource: '/users/{id}',
|
|
146
|
+
path: '/users/123',
|
|
147
|
+
headers: {},
|
|
148
|
+
body: null
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await dispatchEvent(event, routes);
|
|
152
|
+
|
|
153
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
154
|
+
expect.objectContaining({
|
|
155
|
+
params: { id: '123' }
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should parse JSON body', async () => {
|
|
161
|
+
const routes: DispatchRoutes = {
|
|
162
|
+
apigateway: {
|
|
163
|
+
post: {
|
|
164
|
+
'/users': { handler: mockHandler }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const event = {
|
|
170
|
+
requestContext: { requestId: '123' },
|
|
171
|
+
httpMethod: 'POST',
|
|
172
|
+
resource: '/users',
|
|
173
|
+
path: '/users',
|
|
174
|
+
headers: { 'Content-Type': 'application/json' },
|
|
175
|
+
body: JSON.stringify({ name: 'John', email: 'john@example.com' })
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
await dispatchEvent(event, routes);
|
|
179
|
+
|
|
180
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
payload: expect.objectContaining({
|
|
183
|
+
body: { name: 'John', email: 'john@example.com' }
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return 404 when route not found', async () => {
|
|
190
|
+
const routes: DispatchRoutes = {
|
|
191
|
+
apigateway: {
|
|
192
|
+
get: {
|
|
193
|
+
'/users': { handler: mockHandler }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const event = {
|
|
199
|
+
requestContext: { requestId: '123' },
|
|
200
|
+
httpMethod: 'GET',
|
|
201
|
+
resource: '/nonexistent',
|
|
202
|
+
path: '/nonexistent',
|
|
203
|
+
headers: {},
|
|
204
|
+
body: null
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = await dispatchEvent(event, routes);
|
|
208
|
+
|
|
209
|
+
expect(result.statusCode).toBe(404);
|
|
210
|
+
expect(mockHandler).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should execute global middleware', async () => {
|
|
214
|
+
const middlewareFn = jest.fn().mockImplementation((event: NormalizedEvent) => {
|
|
215
|
+
return { ...event, payload: { ...event.payload, modified: true } };
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const routes: DispatchRoutes = {
|
|
219
|
+
apigateway: {
|
|
220
|
+
get: { '/test': { handler: mockHandler } }
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const event = {
|
|
225
|
+
requestContext: { requestId: '123' },
|
|
226
|
+
httpMethod: 'GET',
|
|
227
|
+
resource: '/test',
|
|
228
|
+
path: '/test',
|
|
229
|
+
headers: {},
|
|
230
|
+
body: null
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await dispatchEvent(event, routes, {
|
|
234
|
+
globalMiddleware: [middlewareFn]
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(middlewareFn).toHaveBeenCalledTimes(1);
|
|
238
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('dispatchEvent - EventBridge', () => {
|
|
243
|
+
const mockHandler = jest.fn().mockResolvedValue({ success: true });
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
mockHandler.mockClear();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/** Helper: builds a realistic EventBridge event envelope. */
|
|
250
|
+
const buildEvent = (overrides: Record<string, any> = {}) => ({
|
|
251
|
+
version: '0',
|
|
252
|
+
id: 'evt-12345',
|
|
253
|
+
source: 'envivienda.domain',
|
|
254
|
+
'detail-type': 'user.created',
|
|
255
|
+
detail: { userId: '123' },
|
|
256
|
+
account: '123456789012',
|
|
257
|
+
region: 'us-east-1',
|
|
258
|
+
time: '2026-05-05T05:00:00Z',
|
|
259
|
+
resources: [],
|
|
260
|
+
...overrides,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should dispatch by detail-type (AWS native field)', async () => {
|
|
264
|
+
const routes: DispatchRoutes = {
|
|
265
|
+
eventbridge: { 'user.created': mockHandler },
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await dispatchEvent(buildEvent(), routes);
|
|
269
|
+
|
|
270
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should fallback to detail.operationName for legacy compat', async () => {
|
|
274
|
+
const routes: DispatchRoutes = {
|
|
275
|
+
eventbridge: { 'user.created.legacy': mockHandler },
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Event sin detail-type apropiado pero con operationName legacy
|
|
279
|
+
const event = buildEvent({
|
|
280
|
+
'detail-type': 'something.else',
|
|
281
|
+
detail: { operationName: 'user.created.legacy' },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await dispatchEvent(event, routes);
|
|
285
|
+
|
|
286
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should prefer detail-type over operationName when both are present', async () => {
|
|
290
|
+
const detailTypeHandler = jest.fn().mockResolvedValue({ via: 'detail-type' });
|
|
291
|
+
const opNameHandler = jest.fn().mockResolvedValue({ via: 'opName' });
|
|
292
|
+
|
|
293
|
+
const routes: DispatchRoutes = {
|
|
294
|
+
eventbridge: {
|
|
295
|
+
'preferred.via.detail-type': detailTypeHandler,
|
|
296
|
+
'fallback.via.opName': opNameHandler,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const event = buildEvent({
|
|
301
|
+
'detail-type': 'preferred.via.detail-type',
|
|
302
|
+
detail: { operationName: 'fallback.via.opName' },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
await dispatchEvent(event, routes);
|
|
306
|
+
|
|
307
|
+
expect(detailTypeHandler).toHaveBeenCalledTimes(1);
|
|
308
|
+
expect(opNameHandler).not.toHaveBeenCalled();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should fallback to default handler when nothing matches', async () => {
|
|
312
|
+
const routes: DispatchRoutes = {
|
|
313
|
+
eventbridge: { default: mockHandler },
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const event = buildEvent({
|
|
317
|
+
'detail-type': 'no.such.type',
|
|
318
|
+
detail: {},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await dispatchEvent(event, routes);
|
|
322
|
+
|
|
323
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should pass detail as payload.body to the handler', async () => {
|
|
327
|
+
const routes: DispatchRoutes = {
|
|
328
|
+
eventbridge: { 'user.created': mockHandler },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
await dispatchEvent(buildEvent(), routes);
|
|
332
|
+
|
|
333
|
+
const passedEvent = mockHandler.mock.calls[0][0];
|
|
334
|
+
expect(passedEvent.payload.body).toEqual({ userId: '123' });
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('dispatchEvent - SQS', () => {
|
|
339
|
+
const mockHandler = jest.fn().mockResolvedValue({ success: true });
|
|
340
|
+
|
|
341
|
+
beforeEach(() => {
|
|
342
|
+
mockHandler.mockClear();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should dispatch to queue handler', async () => {
|
|
346
|
+
const routes: DispatchRoutes = {
|
|
347
|
+
sqs: {
|
|
348
|
+
'my-queue': mockHandler
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const event = {
|
|
353
|
+
Records: [
|
|
354
|
+
{
|
|
355
|
+
eventSource: 'aws:sqs',
|
|
356
|
+
eventSourceARN: 'arn:aws:sqs:us-east-1:123456789:my-queue',
|
|
357
|
+
body: JSON.stringify({ message: 'Hello' }),
|
|
358
|
+
messageId: 'msg-123'
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
await dispatchEvent(event, routes);
|
|
364
|
+
|
|
365
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
366
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
payload: expect.objectContaining({
|
|
369
|
+
body: { message: 'Hello' }
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('dispatchEvent - Lambda', () => {
|
|
377
|
+
const mockHandler = jest.fn().mockResolvedValue({ result: 'ok' });
|
|
378
|
+
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
mockHandler.mockClear();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should dispatch to default Lambda handler', async () => {
|
|
384
|
+
const routes: DispatchRoutes = {
|
|
385
|
+
lambda: {
|
|
386
|
+
default: mockHandler
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const event = {
|
|
391
|
+
awsRequestId: '1234-5678',
|
|
392
|
+
customData: { foo: 'bar' }
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
await dispatchEvent(event, routes);
|
|
396
|
+
|
|
397
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('dispatchEvent - Path Parameters Fallback', () => {
|
|
402
|
+
const mockHandler = jest.fn().mockResolvedValue({ statusCode: 200, body: '{}' });
|
|
403
|
+
|
|
404
|
+
beforeEach(() => {
|
|
405
|
+
mockHandler.mockClear();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should include event.pathParameters in params when route matching extracts params', async () => {
|
|
409
|
+
const routes: DispatchRoutes = {
|
|
410
|
+
apigateway: {
|
|
411
|
+
get: {
|
|
412
|
+
'/users/{id}': { handler: mockHandler }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const event = {
|
|
418
|
+
requestContext: { requestId: '123' },
|
|
419
|
+
httpMethod: 'GET',
|
|
420
|
+
resource: '/users/{id}',
|
|
421
|
+
path: '/users/456',
|
|
422
|
+
pathParameters: { id: '456' },
|
|
423
|
+
headers: {},
|
|
424
|
+
body: null
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
await dispatchEvent(event, routes);
|
|
428
|
+
|
|
429
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
430
|
+
expect.objectContaining({
|
|
431
|
+
params: { id: '456' },
|
|
432
|
+
payload: expect.objectContaining({
|
|
433
|
+
pathParameters: { id: '456' }
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should use event.pathParameters as fallback when route matching fails to extract params', async () => {
|
|
440
|
+
const routes: DispatchRoutes = {
|
|
441
|
+
apigateway: {
|
|
442
|
+
public: {
|
|
443
|
+
get: {
|
|
444
|
+
'/property-types/{id}': { handler: mockHandler }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} as SegmentedHttpRouter
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Simulate API Gateway sending full path with basePath, but router uses relative path
|
|
451
|
+
// In this case, route matches by pattern but actualPath differs
|
|
452
|
+
const event = {
|
|
453
|
+
requestContext: { requestId: '123' },
|
|
454
|
+
httpMethod: 'GET',
|
|
455
|
+
resource: '/property-types/{id}',
|
|
456
|
+
path: '/property-types/abc123',
|
|
457
|
+
pathParameters: { id: 'abc123' }, // API Gateway already extracted this
|
|
458
|
+
headers: {},
|
|
459
|
+
body: null
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
await dispatchEvent(event, routes);
|
|
463
|
+
|
|
464
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
465
|
+
expect.objectContaining({
|
|
466
|
+
params: expect.objectContaining({ id: 'abc123' }),
|
|
467
|
+
payload: expect.objectContaining({
|
|
468
|
+
pathParameters: expect.objectContaining({ id: 'abc123' })
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should give priority to extracted params over event.pathParameters', async () => {
|
|
475
|
+
const routes: DispatchRoutes = {
|
|
476
|
+
apigateway: {
|
|
477
|
+
get: {
|
|
478
|
+
'/items/{itemId}': { handler: mockHandler }
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const event = {
|
|
484
|
+
requestContext: { requestId: '123' },
|
|
485
|
+
httpMethod: 'GET',
|
|
486
|
+
resource: '/items/{itemId}',
|
|
487
|
+
path: '/items/extracted-value',
|
|
488
|
+
pathParameters: { itemId: 'original-value', extra: 'should-persist' },
|
|
489
|
+
headers: {},
|
|
490
|
+
body: null
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
await dispatchEvent(event, routes);
|
|
494
|
+
|
|
495
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
496
|
+
expect.objectContaining({
|
|
497
|
+
params: {
|
|
498
|
+
itemId: 'extracted-value', // Extracted takes priority
|
|
499
|
+
extra: 'should-persist' // Original persists
|
|
500
|
+
},
|
|
501
|
+
payload: expect.objectContaining({
|
|
502
|
+
pathParameters: {
|
|
503
|
+
itemId: 'extracted-value',
|
|
504
|
+
extra: 'should-persist'
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should work with basePath mismatch between router and API Gateway', async () => {
|
|
512
|
+
const routes: DispatchRoutes = {
|
|
513
|
+
apigateway: {
|
|
514
|
+
private: {
|
|
515
|
+
get: {
|
|
516
|
+
'/categories/{categoryId}/items/{itemId}': { handler: mockHandler }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} as SegmentedHttpRouter
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const event = {
|
|
523
|
+
requestContext: { requestId: '123' },
|
|
524
|
+
httpMethod: 'GET',
|
|
525
|
+
resource: '/categories/{categoryId}/items/{itemId}',
|
|
526
|
+
path: '/categories/cat-1/items/item-2',
|
|
527
|
+
pathParameters: { categoryId: 'cat-1', itemId: 'item-2' },
|
|
528
|
+
headers: {},
|
|
529
|
+
body: null
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
await dispatchEvent(event, routes);
|
|
533
|
+
|
|
534
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
535
|
+
expect.objectContaining({
|
|
536
|
+
params: { categoryId: 'cat-1', itemId: 'item-2' },
|
|
537
|
+
payload: expect.objectContaining({
|
|
538
|
+
pathParameters: { categoryId: 'cat-1', itemId: 'item-2' }
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should handle null pathParameters from event gracefully', async () => {
|
|
545
|
+
const routes: DispatchRoutes = {
|
|
546
|
+
apigateway: {
|
|
547
|
+
get: {
|
|
548
|
+
'/static-route': { handler: mockHandler }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const event = {
|
|
554
|
+
requestContext: { requestId: '123' },
|
|
555
|
+
httpMethod: 'GET',
|
|
556
|
+
resource: '/static-route',
|
|
557
|
+
path: '/static-route',
|
|
558
|
+
pathParameters: null, // API Gateway sends null for routes without params
|
|
559
|
+
headers: {},
|
|
560
|
+
body: null
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
await dispatchEvent(event, routes);
|
|
564
|
+
|
|
565
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
566
|
+
expect.objectContaining({
|
|
567
|
+
params: {},
|
|
568
|
+
payload: expect.objectContaining({
|
|
569
|
+
pathParameters: {}
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('dispatchEvent - User Pool Validation', () => {
|
|
577
|
+
const mockHandler = jest.fn().mockResolvedValue({ statusCode: 200 });
|
|
578
|
+
|
|
579
|
+
beforeEach(() => {
|
|
580
|
+
mockHandler.mockClear();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('should allow public routes without token', async () => {
|
|
584
|
+
const routes: DispatchRoutes = {
|
|
585
|
+
apigateway: {
|
|
586
|
+
public: {
|
|
587
|
+
get: { '/health': { handler: mockHandler } }
|
|
588
|
+
}
|
|
589
|
+
} as SegmentedHttpRouter
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const event = {
|
|
593
|
+
requestContext: { requestId: '123' },
|
|
594
|
+
httpMethod: 'GET',
|
|
595
|
+
resource: '/health',
|
|
596
|
+
path: '/health',
|
|
597
|
+
headers: {},
|
|
598
|
+
body: null
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
await dispatchEvent(event, routes, {
|
|
602
|
+
userPools: {
|
|
603
|
+
private: 'us-east-1_ABC123'
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should reject private routes with wrong User Pool', async () => {
|
|
611
|
+
const routes: DispatchRoutes = {
|
|
612
|
+
apigateway: {
|
|
613
|
+
private: {
|
|
614
|
+
get: { '/profile': { handler: mockHandler } }
|
|
615
|
+
}
|
|
616
|
+
} as SegmentedHttpRouter
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const event = {
|
|
620
|
+
requestContext: {
|
|
621
|
+
requestId: '123',
|
|
622
|
+
authorizer: {
|
|
623
|
+
claims: {
|
|
624
|
+
sub: 'user-123',
|
|
625
|
+
iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_WRONG'
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
httpMethod: 'GET',
|
|
630
|
+
resource: '/profile',
|
|
631
|
+
path: '/profile',
|
|
632
|
+
headers: {},
|
|
633
|
+
body: null
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const result = await dispatchEvent(event, routes, {
|
|
637
|
+
userPools: {
|
|
638
|
+
private: 'us-east-1_ABC123'
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
expect(result.statusCode).toBe(403);
|
|
643
|
+
expect(mockHandler).not.toHaveBeenCalled();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should allow private routes with correct User Pool', async () => {
|
|
647
|
+
const routes: DispatchRoutes = {
|
|
648
|
+
apigateway: {
|
|
649
|
+
private: {
|
|
650
|
+
get: { '/profile': { handler: mockHandler } }
|
|
651
|
+
}
|
|
652
|
+
} as SegmentedHttpRouter
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const event = {
|
|
656
|
+
requestContext: {
|
|
657
|
+
requestId: '123',
|
|
658
|
+
authorizer: {
|
|
659
|
+
claims: {
|
|
660
|
+
sub: 'user-123',
|
|
661
|
+
iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
httpMethod: 'GET',
|
|
666
|
+
resource: '/profile',
|
|
667
|
+
path: '/profile',
|
|
668
|
+
headers: {},
|
|
669
|
+
body: null
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
await dispatchEvent(event, routes, {
|
|
673
|
+
userPools: {
|
|
674
|
+
private: 'us-east-1_ABC123'
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe('dispatchEvent - Internal Segment', () => {
|
|
683
|
+
const mockHandler = jest.fn().mockResolvedValue({ statusCode: 200, body: '{}' });
|
|
684
|
+
|
|
685
|
+
beforeEach(() => {
|
|
686
|
+
mockHandler.mockClear();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should dispatch to internal segment routes', async () => {
|
|
690
|
+
const routes: DispatchRoutes = {
|
|
691
|
+
apigateway: {
|
|
692
|
+
internal: {
|
|
693
|
+
get: { '/internal/users/{id}': { handler: mockHandler } }
|
|
694
|
+
}
|
|
695
|
+
} as SegmentedHttpRouter
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const event = {
|
|
699
|
+
requestContext: { requestId: '123' },
|
|
700
|
+
httpMethod: 'GET',
|
|
701
|
+
resource: '/internal/users/{id}',
|
|
702
|
+
path: '/internal/users/user-123',
|
|
703
|
+
pathParameters: { id: 'user-123' },
|
|
704
|
+
headers: {},
|
|
705
|
+
body: null
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
await dispatchEvent(event, routes);
|
|
709
|
+
|
|
710
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
711
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
712
|
+
expect.objectContaining({
|
|
713
|
+
context: expect.objectContaining({
|
|
714
|
+
segment: RouteSegment.Internal
|
|
715
|
+
}),
|
|
716
|
+
params: { id: 'user-123' },
|
|
717
|
+
payload: expect.objectContaining({
|
|
718
|
+
pathParameters: { id: 'user-123' }
|
|
719
|
+
})
|
|
720
|
+
})
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('should extract path parameters from internal routes with IAM auth (no Cognito)', async () => {
|
|
725
|
+
const routes: DispatchRoutes = {
|
|
726
|
+
apigateway: {
|
|
727
|
+
internal: {
|
|
728
|
+
get: { '/internal/users/{id}': { handler: mockHandler } }
|
|
729
|
+
}
|
|
730
|
+
} as SegmentedHttpRouter
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// Simulate Internal API Gateway event with IAM auth (no authorizer/claims)
|
|
734
|
+
const event = {
|
|
735
|
+
requestContext: {
|
|
736
|
+
requestId: 'iam-request-123',
|
|
737
|
+
identity: {
|
|
738
|
+
userArn: 'arn:aws:iam::123456789:user/lambda-role'
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
httpMethod: 'GET',
|
|
742
|
+
resource: '/internal/users/{id}',
|
|
743
|
+
path: '/internal/users/be018a15-4a51-45b1-b610-d7eb9430b50f',
|
|
744
|
+
pathParameters: { id: 'be018a15-4a51-45b1-b610-d7eb9430b50f' },
|
|
745
|
+
headers: {
|
|
746
|
+
'X-Trace-Id': 'trace-123',
|
|
747
|
+
'X-Source-Lambda': 'ml-agent-manager-lambda'
|
|
748
|
+
},
|
|
749
|
+
body: null
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
await dispatchEvent(event, routes);
|
|
753
|
+
|
|
754
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
755
|
+
expect(mockHandler).toHaveBeenCalledWith(
|
|
756
|
+
expect.objectContaining({
|
|
757
|
+
params: { id: 'be018a15-4a51-45b1-b610-d7eb9430b50f' },
|
|
758
|
+
payload: expect.objectContaining({
|
|
759
|
+
pathParameters: { id: 'be018a15-4a51-45b1-b610-d7eb9430b50f' }
|
|
760
|
+
}),
|
|
761
|
+
context: expect.objectContaining({
|
|
762
|
+
segment: RouteSegment.Internal
|
|
763
|
+
})
|
|
764
|
+
})
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('should not require User Pool validation for internal segment', async () => {
|
|
769
|
+
const routes: DispatchRoutes = {
|
|
770
|
+
apigateway: {
|
|
771
|
+
internal: {
|
|
772
|
+
get: { '/internal/data': { handler: mockHandler } }
|
|
773
|
+
}
|
|
774
|
+
} as SegmentedHttpRouter
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const event = {
|
|
778
|
+
requestContext: { requestId: '123' },
|
|
779
|
+
httpMethod: 'GET',
|
|
780
|
+
resource: '/internal/data',
|
|
781
|
+
path: '/internal/data',
|
|
782
|
+
headers: {},
|
|
783
|
+
body: null
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
// Even with userPools configured, internal routes should not require Cognito validation
|
|
787
|
+
await dispatchEvent(event, routes, {
|
|
788
|
+
userPools: {
|
|
789
|
+
private: 'us-east-1_ABC123'
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
expect(mockHandler).toHaveBeenCalledTimes(1);
|
|
794
|
+
});
|
|
795
|
+
});
|