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