serverless-event-orchestrator 1.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/dispatcher.d.ts +18 -0
  4. package/dist/dispatcher.d.ts.map +1 -0
  5. package/dist/dispatcher.js +345 -0
  6. package/dist/dispatcher.js.map +1 -0
  7. package/dist/http/body-parser.d.ts +27 -0
  8. package/dist/http/body-parser.d.ts.map +1 -0
  9. package/dist/http/body-parser.js +56 -0
  10. package/dist/http/body-parser.js.map +1 -0
  11. package/dist/http/cors.d.ts +32 -0
  12. package/dist/http/cors.d.ts.map +1 -0
  13. package/dist/http/cors.js +69 -0
  14. package/dist/http/cors.js.map +1 -0
  15. package/dist/http/index.d.ts +4 -0
  16. package/dist/http/index.d.ts.map +1 -0
  17. package/dist/http/index.js +20 -0
  18. package/dist/http/index.js.map +1 -0
  19. package/dist/http/response.d.ts +104 -0
  20. package/dist/http/response.d.ts.map +1 -0
  21. package/dist/http/response.js +164 -0
  22. package/dist/http/response.js.map +1 -0
  23. package/dist/identity/extractor.d.ts +39 -0
  24. package/dist/identity/extractor.d.ts.map +1 -0
  25. package/dist/identity/extractor.js +88 -0
  26. package/dist/identity/extractor.js.map +1 -0
  27. package/dist/identity/index.d.ts +2 -0
  28. package/dist/identity/index.d.ts.map +1 -0
  29. package/dist/identity/index.js +18 -0
  30. package/dist/identity/index.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +62 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/types/event-type.enum.d.ts +20 -0
  36. package/dist/types/event-type.enum.d.ts.map +1 -0
  37. package/dist/types/event-type.enum.js +25 -0
  38. package/dist/types/event-type.enum.js.map +1 -0
  39. package/dist/types/index.d.ts +3 -0
  40. package/dist/types/index.d.ts.map +1 -0
  41. package/dist/types/index.js +19 -0
  42. package/dist/types/index.js.map +1 -0
  43. package/dist/types/routes.d.ts +163 -0
  44. package/dist/types/routes.d.ts.map +1 -0
  45. package/dist/types/routes.js +3 -0
  46. package/dist/types/routes.js.map +1 -0
  47. package/dist/utils/headers.d.ts +28 -0
  48. package/dist/utils/headers.d.ts.map +1 -0
  49. package/dist/utils/headers.js +61 -0
  50. package/dist/utils/headers.js.map +1 -0
  51. package/dist/utils/index.d.ts +3 -0
  52. package/dist/utils/index.d.ts.map +1 -0
  53. package/dist/utils/index.js +19 -0
  54. package/dist/utils/index.js.map +1 -0
  55. package/dist/utils/path-matcher.d.ts +33 -0
  56. package/dist/utils/path-matcher.d.ts.map +1 -0
  57. package/dist/utils/path-matcher.js +74 -0
  58. package/dist/utils/path-matcher.js.map +1 -0
  59. package/jest.config.js +32 -0
  60. package/package.json +68 -0
  61. package/src/dispatcher.ts +415 -0
  62. package/src/http/body-parser.ts +60 -0
  63. package/src/http/cors.ts +76 -0
  64. package/src/http/index.ts +3 -0
  65. package/src/http/response.ts +194 -0
  66. package/src/identity/extractor.ts +89 -0
  67. package/src/identity/index.ts +1 -0
  68. package/src/index.ts +92 -0
  69. package/src/types/event-type.enum.ts +20 -0
  70. package/src/types/index.ts +2 -0
  71. package/src/types/routes.ts +182 -0
  72. package/src/utils/headers.ts +72 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/path-matcher.ts +79 -0
  75. package/tests/cors.test.ts +133 -0
  76. package/tests/dispatcher.test.ts +425 -0
  77. package/tests/headers.test.ts +99 -0
  78. package/tests/identity.test.ts +171 -0
  79. package/tests/path-matcher.test.ts +102 -0
  80. package/tests/response.test.ts +155 -0
  81. package/tsconfig.json +24 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Path matching utilities for extracting path parameters
3
+ * Supports patterns like /users/{id} and /users/{userId}/posts/{postId}
4
+ */
5
+
6
+ /**
7
+ * Converts a route pattern to a regex and extracts parameter names
8
+ * @param pattern - Route pattern like /users/{id}
9
+ * @returns Object with regex and parameter names
10
+ */
11
+ export function patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {
12
+ const paramNames: string[] = [];
13
+
14
+ // Escape special regex characters except for our parameter syntax
15
+ let regexPattern = pattern
16
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
17
+ .replace(/\\\{(\w+)\\\}/g, (_, paramName) => {
18
+ paramNames.push(paramName);
19
+ return '([^/]+)';
20
+ });
21
+
22
+ // Ensure exact match
23
+ regexPattern = `^${regexPattern}$`;
24
+
25
+ return {
26
+ regex: new RegExp(regexPattern),
27
+ paramNames,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Matches a path against a pattern and extracts parameters
33
+ * @param pattern - Route pattern like /users/{id}
34
+ * @param path - Actual path like /users/123
35
+ * @returns Extracted parameters or null if no match
36
+ */
37
+ export function matchPath(pattern: string, path: string): Record<string, string> | null {
38
+ const { regex, paramNames } = patternToRegex(pattern);
39
+ const match = path.match(regex);
40
+
41
+ if (!match) {
42
+ return null;
43
+ }
44
+
45
+ const params: Record<string, string> = {};
46
+ paramNames.forEach((name, index) => {
47
+ params[name] = match[index + 1];
48
+ });
49
+
50
+ return params;
51
+ }
52
+
53
+ /**
54
+ * Checks if a pattern contains path parameters
55
+ * @param pattern - Route pattern to check
56
+ * @returns True if pattern has parameters
57
+ */
58
+ export function hasPathParameters(pattern: string): boolean {
59
+ return /\{[\w]+\}/.test(pattern);
60
+ }
61
+
62
+ /**
63
+ * Normalizes a path by removing trailing slashes and ensuring leading slash
64
+ * @param path - Path to normalize
65
+ * @returns Normalized path
66
+ */
67
+ export function normalizePath(path: string): string {
68
+ if (!path) return '/';
69
+
70
+ // Ensure leading slash
71
+ let normalized = path.startsWith('/') ? path : `/${path}`;
72
+
73
+ // Remove trailing slash (except for root)
74
+ if (normalized.length > 1 && normalized.endsWith('/')) {
75
+ normalized = normalized.slice(0, -1);
76
+ }
77
+
78
+ return normalized;
79
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ isPreflightRequest,
3
+ createPreflightResponse,
4
+ applyCorsHeaders,
5
+ withCors
6
+ } from '../src/http/cors';
7
+ import { HttpResponse } from '../src/http/response';
8
+
9
+ describe('isPreflightRequest', () => {
10
+ it('should return true for OPTIONS request', () => {
11
+ const event = { httpMethod: 'OPTIONS' };
12
+ expect(isPreflightRequest(event)).toBe(true);
13
+ });
14
+
15
+ it('should return true for lowercase options', () => {
16
+ const event = { httpMethod: 'options' };
17
+ expect(isPreflightRequest(event)).toBe(true);
18
+ });
19
+
20
+ it('should return false for GET request', () => {
21
+ const event = { httpMethod: 'GET' };
22
+ expect(isPreflightRequest(event)).toBe(false);
23
+ });
24
+
25
+ it('should return false for POST request', () => {
26
+ const event = { httpMethod: 'POST' };
27
+ expect(isPreflightRequest(event)).toBe(false);
28
+ });
29
+
30
+ it('should return false for undefined httpMethod', () => {
31
+ const event = {};
32
+ expect(isPreflightRequest(event)).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe('createPreflightResponse', () => {
37
+ it('should return 204 with default CORS headers', () => {
38
+ const response = createPreflightResponse(true);
39
+
40
+ expect(response.statusCode).toBe(204);
41
+ expect(response.body).toBe('');
42
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
43
+ expect(response.headers?.['Access-Control-Allow-Methods']).toContain('GET');
44
+ });
45
+
46
+ it('should use custom CORS config', () => {
47
+ const response = createPreflightResponse({
48
+ origins: ['https://myapp.com'],
49
+ methods: ['GET', 'POST'],
50
+ credentials: true
51
+ });
52
+
53
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
54
+ expect(response.headers?.['Access-Control-Allow-Methods']).toBe('GET, POST');
55
+ expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
56
+ });
57
+ });
58
+
59
+ describe('applyCorsHeaders', () => {
60
+ const originalResponse: HttpResponse = {
61
+ statusCode: 200,
62
+ body: JSON.stringify({ data: 'test' }),
63
+ headers: { 'Content-Type': 'application/json' }
64
+ };
65
+
66
+ it('should add CORS headers to response', () => {
67
+ const response = applyCorsHeaders(originalResponse, true);
68
+
69
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
70
+ expect(response.headers?.['Content-Type']).toBe('application/json');
71
+ });
72
+
73
+ it('should not modify response when cors is false', () => {
74
+ const response = applyCorsHeaders(originalResponse, false);
75
+
76
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBeUndefined();
77
+ });
78
+
79
+ it('should use custom CORS config', () => {
80
+ const response = applyCorsHeaders(originalResponse, {
81
+ origins: ['https://example.com'],
82
+ maxAge: 3600
83
+ });
84
+
85
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://example.com');
86
+ expect(response.headers?.['Access-Control-Max-Age']).toBe('3600');
87
+ });
88
+ });
89
+
90
+ describe('withCors', () => {
91
+ const mockHandler = jest.fn().mockResolvedValue({
92
+ statusCode: 200,
93
+ body: JSON.stringify({ success: true })
94
+ });
95
+
96
+ beforeEach(() => {
97
+ mockHandler.mockClear();
98
+ });
99
+
100
+ it('should handle preflight requests without calling handler', async () => {
101
+ const wrappedHandler = withCors(mockHandler, true);
102
+
103
+ const event = { eventRaw: { httpMethod: 'OPTIONS' } };
104
+ const response = await wrappedHandler(event);
105
+
106
+ expect(response.statusCode).toBe(204);
107
+ expect(mockHandler).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('should call handler and add CORS headers for non-preflight', async () => {
111
+ const wrappedHandler = withCors(mockHandler, true);
112
+
113
+ const event = { eventRaw: { httpMethod: 'GET' } };
114
+ const response = await wrappedHandler(event);
115
+
116
+ expect(mockHandler).toHaveBeenCalledTimes(1);
117
+ expect(response.statusCode).toBe(200);
118
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('*');
119
+ });
120
+
121
+ it('should work with custom CORS config', async () => {
122
+ const wrappedHandler = withCors(mockHandler, {
123
+ origins: ['https://myapp.com'],
124
+ credentials: true
125
+ });
126
+
127
+ const event = { eventRaw: { httpMethod: 'POST' } };
128
+ const response = await wrappedHandler(event);
129
+
130
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://myapp.com');
131
+ expect(response.headers?.['Access-Control-Allow-Credentials']).toBe('true');
132
+ });
133
+ });
@@ -0,0 +1,425 @@
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 - User Pool Validation', () => {
322
+ const mockHandler = jest.fn().mockResolvedValue({ statusCode: 200 });
323
+
324
+ beforeEach(() => {
325
+ mockHandler.mockClear();
326
+ });
327
+
328
+ it('should allow public routes without token', async () => {
329
+ const routes: DispatchRoutes = {
330
+ apigateway: {
331
+ public: {
332
+ get: { '/health': { handler: mockHandler } }
333
+ }
334
+ } as SegmentedHttpRouter
335
+ };
336
+
337
+ const event = {
338
+ requestContext: { requestId: '123' },
339
+ httpMethod: 'GET',
340
+ resource: '/health',
341
+ path: '/health',
342
+ headers: {},
343
+ body: null
344
+ };
345
+
346
+ await dispatchEvent(event, routes, {
347
+ userPools: {
348
+ private: 'us-east-1_ABC123'
349
+ }
350
+ });
351
+
352
+ expect(mockHandler).toHaveBeenCalledTimes(1);
353
+ });
354
+
355
+ it('should reject private routes with wrong User Pool', async () => {
356
+ const routes: DispatchRoutes = {
357
+ apigateway: {
358
+ private: {
359
+ get: { '/profile': { handler: mockHandler } }
360
+ }
361
+ } as SegmentedHttpRouter
362
+ };
363
+
364
+ const event = {
365
+ requestContext: {
366
+ requestId: '123',
367
+ authorizer: {
368
+ claims: {
369
+ sub: 'user-123',
370
+ iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_WRONG'
371
+ }
372
+ }
373
+ },
374
+ httpMethod: 'GET',
375
+ resource: '/profile',
376
+ path: '/profile',
377
+ headers: {},
378
+ body: null
379
+ };
380
+
381
+ const result = await dispatchEvent(event, routes, {
382
+ userPools: {
383
+ private: 'us-east-1_ABC123'
384
+ }
385
+ });
386
+
387
+ expect(result.statusCode).toBe(403);
388
+ expect(mockHandler).not.toHaveBeenCalled();
389
+ });
390
+
391
+ it('should allow private routes with correct User Pool', async () => {
392
+ const routes: DispatchRoutes = {
393
+ apigateway: {
394
+ private: {
395
+ get: { '/profile': { handler: mockHandler } }
396
+ }
397
+ } as SegmentedHttpRouter
398
+ };
399
+
400
+ const event = {
401
+ requestContext: {
402
+ requestId: '123',
403
+ authorizer: {
404
+ claims: {
405
+ sub: 'user-123',
406
+ iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
407
+ }
408
+ }
409
+ },
410
+ httpMethod: 'GET',
411
+ resource: '/profile',
412
+ path: '/profile',
413
+ headers: {},
414
+ body: null
415
+ };
416
+
417
+ await dispatchEvent(event, routes, {
418
+ userPools: {
419
+ private: 'us-east-1_ABC123'
420
+ }
421
+ });
422
+
423
+ expect(mockHandler).toHaveBeenCalledTimes(1);
424
+ });
425
+ });