visus-mcp 0.2.0 → 0.6.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 (107) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/LINKEDIN-STRATEGY.md +367 -0
  3. package/README.md +491 -16
  4. package/ROADMAP.md +214 -34
  5. package/SECURITY-AUDIT-v1.md +277 -0
  6. package/STATUS.md +801 -42
  7. package/TROUBLESHOOT-AUTH-20260322-2019.md +291 -0
  8. package/TROUBLESHOOT-JEST-20260323-1357.md +139 -0
  9. package/TROUBLESHOOT-LAMBDA-20260322-1945.md +183 -0
  10. package/VISUS-CLAUDE-CODE-PROMPT.md +1 -1
  11. package/VISUS-PROJECT-PLAN.md +7 -0
  12. package/dist/browser/playwright-renderer.d.ts.map +1 -1
  13. package/dist/browser/playwright-renderer.js +7 -0
  14. package/dist/browser/playwright-renderer.js.map +1 -1
  15. package/dist/browser/reader.d.ts +31 -0
  16. package/dist/browser/reader.d.ts.map +1 -0
  17. package/dist/browser/reader.js +98 -0
  18. package/dist/browser/reader.js.map +1 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +37 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/lambda-handler.d.ts +0 -6
  24. package/dist/lambda-handler.d.ts.map +1 -1
  25. package/dist/lambda-handler.js +97 -25
  26. package/dist/lambda-handler.js.map +1 -1
  27. package/dist/sanitizer/framework-mapper.d.ts +22 -0
  28. package/dist/sanitizer/framework-mapper.d.ts.map +1 -0
  29. package/dist/sanitizer/framework-mapper.js +296 -0
  30. package/dist/sanitizer/framework-mapper.js.map +1 -0
  31. package/dist/sanitizer/index.d.ts +10 -2
  32. package/dist/sanitizer/index.d.ts.map +1 -1
  33. package/dist/sanitizer/index.js +22 -6
  34. package/dist/sanitizer/index.js.map +1 -1
  35. package/dist/sanitizer/patterns.js +1 -1
  36. package/dist/sanitizer/patterns.js.map +1 -1
  37. package/dist/sanitizer/pii-allowlist.d.ts +49 -0
  38. package/dist/sanitizer/pii-allowlist.d.ts.map +1 -0
  39. package/dist/sanitizer/pii-allowlist.js +231 -0
  40. package/dist/sanitizer/pii-allowlist.js.map +1 -0
  41. package/dist/sanitizer/pii-redactor.d.ts +13 -1
  42. package/dist/sanitizer/pii-redactor.d.ts.map +1 -1
  43. package/dist/sanitizer/pii-redactor.js +26 -2
  44. package/dist/sanitizer/pii-redactor.js.map +1 -1
  45. package/dist/sanitizer/severity-classifier.d.ts +33 -0
  46. package/dist/sanitizer/severity-classifier.d.ts.map +1 -0
  47. package/dist/sanitizer/severity-classifier.js +113 -0
  48. package/dist/sanitizer/severity-classifier.js.map +1 -0
  49. package/dist/sanitizer/threat-reporter.d.ts +65 -0
  50. package/dist/sanitizer/threat-reporter.d.ts.map +1 -0
  51. package/dist/sanitizer/threat-reporter.js +160 -0
  52. package/dist/sanitizer/threat-reporter.js.map +1 -0
  53. package/dist/tools/fetch-structured.d.ts +5 -0
  54. package/dist/tools/fetch-structured.d.ts.map +1 -1
  55. package/dist/tools/fetch-structured.js +59 -8
  56. package/dist/tools/fetch-structured.js.map +1 -1
  57. package/dist/tools/fetch.d.ts +5 -0
  58. package/dist/tools/fetch.d.ts.map +1 -1
  59. package/dist/tools/fetch.js +43 -9
  60. package/dist/tools/fetch.js.map +1 -1
  61. package/dist/tools/read.d.ts +51 -0
  62. package/dist/tools/read.d.ts.map +1 -0
  63. package/dist/tools/read.js +127 -0
  64. package/dist/tools/read.js.map +1 -0
  65. package/dist/tools/search.d.ts +45 -0
  66. package/dist/tools/search.d.ts.map +1 -0
  67. package/dist/tools/search.js +220 -0
  68. package/dist/tools/search.js.map +1 -0
  69. package/dist/types.d.ts +74 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/dist/utils/format-converter.d.ts +39 -0
  73. package/dist/utils/format-converter.d.ts.map +1 -0
  74. package/dist/utils/format-converter.js +191 -0
  75. package/dist/utils/format-converter.js.map +1 -0
  76. package/dist/utils/truncate.d.ts +26 -0
  77. package/dist/utils/truncate.d.ts.map +1 -0
  78. package/dist/utils/truncate.js +54 -0
  79. package/dist/utils/truncate.js.map +1 -0
  80. package/infrastructure/stack.ts +55 -6
  81. package/jest.config.js +3 -0
  82. package/package.json +9 -2
  83. package/src/browser/playwright-renderer.ts +8 -0
  84. package/src/browser/reader.ts +129 -0
  85. package/src/index.ts +49 -5
  86. package/src/lambda-handler.ts +131 -26
  87. package/src/sanitizer/framework-mapper.ts +347 -0
  88. package/src/sanitizer/index.ts +28 -6
  89. package/src/sanitizer/patterns.ts +1 -1
  90. package/src/sanitizer/pii-allowlist.ts +273 -0
  91. package/src/sanitizer/pii-redactor.ts +43 -2
  92. package/src/sanitizer/severity-classifier.ts +132 -0
  93. package/src/sanitizer/threat-reporter.ts +261 -0
  94. package/src/tools/fetch-structured.ts +63 -8
  95. package/src/tools/fetch.ts +45 -9
  96. package/src/tools/read.ts +143 -0
  97. package/src/tools/search.ts +263 -0
  98. package/src/types.ts +71 -0
  99. package/src/utils/format-converter.ts +236 -0
  100. package/src/utils/truncate.ts +64 -0
  101. package/tests/auth-smoke.test.ts +480 -0
  102. package/tests/fetch-tool.test.ts +595 -2
  103. package/tests/pii-allowlist.test.ts +282 -0
  104. package/tests/reader.test.ts +353 -0
  105. package/tests/sanitizer.test.ts +52 -0
  106. package/tests/search.test.ts +456 -0
  107. package/tests/threat-reporter.test.ts +266 -0
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Authentication Enforcement Smoke Tests
3
+ *
4
+ * These tests verify that authentication is properly enforced across all
5
+ * API endpoints and Lambda invocation paths per CLAUDE.md security rules.
6
+ *
7
+ * Test Categories:
8
+ * 1. API Gateway Cognito Authorizer enforcement
9
+ * 2. Lambda handler behavior with/without auth context
10
+ * 3. Health endpoint bypass (intentional)
11
+ * 4. CORS enforcement
12
+ * 5. User ID extraction and audit logging
13
+ * 6. Direct Lambda invocation (bypass prevention)
14
+ */
15
+
16
+ import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
17
+ import { handler } from '../src/lambda-handler.js';
18
+
19
+ /**
20
+ * Mock API Gateway event builder
21
+ */
22
+ function createMockEvent(
23
+ path: string,
24
+ httpMethod: string,
25
+ body: Record<string, unknown> | null,
26
+ authContext?: {
27
+ sub: string;
28
+ email?: string;
29
+ }
30
+ ): APIGatewayProxyEvent {
31
+ const event: Partial<APIGatewayProxyEvent> = {
32
+ path,
33
+ httpMethod,
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'User-Agent': 'jest/smoke-test',
37
+ origin: 'https://claude.ai',
38
+ },
39
+ body: body ? JSON.stringify(body) : null,
40
+ requestContext: {
41
+ requestId: 'test-request-id',
42
+ identity: {
43
+ sourceIp: '127.0.0.1',
44
+ } as any,
45
+ authorizer: authContext ? { claims: authContext } : undefined,
46
+ } as any,
47
+ };
48
+
49
+ return event as APIGatewayProxyEvent;
50
+ }
51
+
52
+ /**
53
+ * Mock Lambda context
54
+ */
55
+ const mockContext: Context = {
56
+ awsRequestId: 'test-request-id',
57
+ functionName: 'visus-mcp-test',
58
+ functionVersion: '1',
59
+ invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:visus-mcp-test',
60
+ memoryLimitInMB: '1024',
61
+ logGroupName: '/aws/lambda/visus-mcp-test',
62
+ logStreamName: 'test-stream',
63
+ callbackWaitsForEmptyEventLoop: false,
64
+ getRemainingTimeInMillis: () => 30000,
65
+ done: () => {},
66
+ fail: () => {},
67
+ succeed: () => {},
68
+ };
69
+
70
+ describe('Authentication Enforcement Smoke Tests', () => {
71
+ // Mock environment variables
72
+ beforeAll(() => {
73
+ process.env.AUDIT_TABLE_NAME = 'visus-audit-test';
74
+ process.env.ENVIRONMENT = 'test';
75
+ process.env.ALLOWED_ORIGINS = 'https://claude.ai,https://app.claude.ai';
76
+ });
77
+
78
+ afterAll(() => {
79
+ delete process.env.AUDIT_TABLE_NAME;
80
+ delete process.env.ENVIRONMENT;
81
+ delete process.env.ALLOWED_ORIGINS;
82
+ });
83
+
84
+ describe('1. Health Endpoint (Unauthenticated Access Allowed)', () => {
85
+ it('should allow /health with GET without auth context', async () => {
86
+ const event = createMockEvent('/health', 'GET', null);
87
+
88
+ const response = await handler(event, mockContext);
89
+
90
+ expect(response.statusCode).toBe(200);
91
+ const body = JSON.parse(response.body);
92
+ expect(body.status).toBe('healthy');
93
+ expect(body.service).toBe('visus-mcp');
94
+ expect(body.version).toBe('0.3.1');
95
+ });
96
+
97
+ it('should allow /health with POST without auth context', async () => {
98
+ const event = createMockEvent('/health', 'POST', {});
99
+
100
+ const response = await handler(event, mockContext);
101
+
102
+ expect(response.statusCode).toBe(200);
103
+ const body = JSON.parse(response.body);
104
+ expect(body.status).toBe('healthy');
105
+ });
106
+
107
+ it('should allow /dev/health without auth context', async () => {
108
+ const event = createMockEvent('/dev/health', 'GET', null);
109
+
110
+ const response = await handler(event, mockContext);
111
+
112
+ expect(response.statusCode).toBe(200);
113
+ const body = JSON.parse(response.body);
114
+ expect(body.status).toBe('healthy');
115
+ });
116
+
117
+ it('should allow /prod/health without auth context', async () => {
118
+ const event = createMockEvent('/prod/health', 'GET', null);
119
+
120
+ const response = await handler(event, mockContext);
121
+
122
+ expect(response.statusCode).toBe(200);
123
+ const body = JSON.parse(response.body);
124
+ expect(body.status).toBe('healthy');
125
+ });
126
+ });
127
+
128
+ describe('2. Protected Endpoints WITHOUT Auth Context', () => {
129
+ it('should REJECT /fetch requests without auth (SECURITY FIX - FINDING 1)', async () => {
130
+ const event = createMockEvent('/fetch', 'POST', {
131
+ url: 'https://example.com',
132
+ });
133
+
134
+ const response = await handler(event, mockContext);
135
+
136
+ // FIXED: Lambda now enforces auth at application level
137
+ expect(response.statusCode).toBe(401);
138
+ const body = JSON.parse(response.body);
139
+ expect(body.error).toContain('Unauthorized');
140
+ expect(body.error).toContain('Authentication required');
141
+ });
142
+
143
+ it('should REJECT /fetch-structured requests without auth (SECURITY FIX - FINDING 1)', async () => {
144
+ const event = createMockEvent('/fetch-structured', 'POST', {
145
+ url: 'https://example.com',
146
+ schema: { title: 'h1' },
147
+ });
148
+
149
+ const response = await handler(event, mockContext);
150
+
151
+ // FIXED: Lambda now enforces auth at application level
152
+ expect(response.statusCode).toBe(401);
153
+ const body = JSON.parse(response.body);
154
+ expect(body.error).toContain('Unauthorized');
155
+ expect(body.error).toContain('Authentication required');
156
+ });
157
+
158
+ it('should log auth_required event when no auth context present', async () => {
159
+ const event = createMockEvent('/fetch', 'POST', {
160
+ url: 'https://example.com',
161
+ });
162
+
163
+ // Capture console.error calls to verify auth logging
164
+ const originalConsoleError = console.error;
165
+ const loggedEvents: string[] = [];
166
+ console.error = (message: string) => {
167
+ loggedEvents.push(message);
168
+ };
169
+
170
+ await handler(event, mockContext);
171
+
172
+ console.error = originalConsoleError;
173
+
174
+ // Verify that auth_required event was logged
175
+ const authLog = loggedEvents.find((log) => {
176
+ try {
177
+ const parsed = JSON.parse(log);
178
+ return parsed.event === 'auth_required';
179
+ } catch {
180
+ return false;
181
+ }
182
+ });
183
+
184
+ expect(authLog).toBeDefined();
185
+ if (authLog) {
186
+ const parsed = JSON.parse(authLog);
187
+ expect(parsed.reason).toContain('Cognito authorizer');
188
+ }
189
+ });
190
+ });
191
+
192
+ describe('3. Protected Endpoints WITH Auth Context', () => {
193
+ it('should extract user_id from Cognito authorizer claims', async () => {
194
+ const authContext = {
195
+ sub: 'test-user-123',
196
+ email: 'test@example.com',
197
+ };
198
+
199
+ const event = createMockEvent(
200
+ '/fetch',
201
+ 'POST',
202
+ { url: 'https://example.com' },
203
+ authContext
204
+ );
205
+
206
+ // Capture console.error to verify user_id is extracted
207
+ const originalConsoleError = console.error;
208
+ const loggedEvents: string[] = [];
209
+ console.error = (message: string) => {
210
+ loggedEvents.push(message);
211
+ };
212
+
213
+ await handler(event, mockContext);
214
+
215
+ console.error = originalConsoleError;
216
+
217
+ // User ID extraction happens at line 132 of lambda-handler.ts
218
+ // We can't directly inspect it, but we can verify the handler doesn't crash
219
+ // and processes the request normally
220
+ expect(loggedEvents.length).toBeGreaterThan(0);
221
+ });
222
+
223
+ it('should process /fetch with valid auth context', async () => {
224
+ const authContext = {
225
+ sub: 'test-user-123',
226
+ };
227
+
228
+ const event = createMockEvent(
229
+ '/fetch',
230
+ 'POST',
231
+ { url: 'https://example.com' },
232
+ authContext
233
+ );
234
+
235
+ const response = await handler(event, mockContext);
236
+
237
+ // Should succeed (or fail with a valid error, not 401/403)
238
+ expect([200, 400, 500]).toContain(response.statusCode);
239
+ });
240
+
241
+ it('should process /fetch-structured with valid auth context', async () => {
242
+ const authContext = {
243
+ sub: 'test-user-456',
244
+ };
245
+
246
+ const event = createMockEvent(
247
+ '/fetch-structured',
248
+ 'POST',
249
+ { url: 'https://example.com', schema: { title: 'h1' } },
250
+ authContext
251
+ );
252
+
253
+ const response = await handler(event, mockContext);
254
+
255
+ // Should succeed (or fail with a valid error, not 401/403)
256
+ expect([200, 400, 500]).toContain(response.statusCode);
257
+ });
258
+ });
259
+
260
+ describe('4. CORS Enforcement', () => {
261
+ it('should validate origin against allowlist', async () => {
262
+ const event = createMockEvent('/health', 'GET', null);
263
+ event.headers.origin = 'https://malicious-site.com';
264
+
265
+ const response = await handler(event, mockContext);
266
+
267
+ // CORS headers should use first allowed origin, not the malicious one
268
+ expect(response.headers?.['Access-Control-Allow-Origin']).not.toBe(
269
+ 'https://malicious-site.com'
270
+ );
271
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe('https://claude.ai');
272
+ });
273
+
274
+ it('should allow whitelisted origin', async () => {
275
+ const event = createMockEvent('/health', 'GET', null);
276
+ event.headers.origin = 'https://app.claude.ai';
277
+
278
+ const response = await handler(event, mockContext);
279
+
280
+ expect(response.headers?.['Access-Control-Allow-Origin']).toBe(
281
+ 'https://app.claude.ai'
282
+ );
283
+ });
284
+
285
+ it('should handle OPTIONS preflight request', async () => {
286
+ const event = createMockEvent('/fetch', 'OPTIONS', null);
287
+
288
+ const response = await handler(event, mockContext);
289
+
290
+ expect(response.statusCode).toBe(200);
291
+ expect(response.headers?.['Access-Control-Allow-Methods']).toBe('GET, POST, OPTIONS');
292
+ expect(response.headers?.['Access-Control-Allow-Headers']).toContain('Authorization');
293
+ });
294
+ });
295
+
296
+ describe('5. Method Enforcement', () => {
297
+ it('should reject GET requests to /fetch', async () => {
298
+ const event = createMockEvent('/fetch', 'GET', null);
299
+
300
+ const response = await handler(event, mockContext);
301
+
302
+ expect(response.statusCode).toBe(405);
303
+ const body = JSON.parse(response.body);
304
+ expect(body.error).toContain('Method not allowed');
305
+ });
306
+
307
+ it('should reject PUT requests to /fetch-structured', async () => {
308
+ const event = createMockEvent('/fetch-structured', 'PUT', null);
309
+
310
+ const response = await handler(event, mockContext);
311
+
312
+ expect(response.statusCode).toBe(405);
313
+ });
314
+
315
+ it('should reject DELETE requests', async () => {
316
+ const event = createMockEvent('/fetch', 'DELETE', null);
317
+
318
+ const response = await handler(event, mockContext);
319
+
320
+ expect(response.statusCode).toBe(405);
321
+ });
322
+ });
323
+
324
+ describe('6. Input Validation', () => {
325
+ it('should reject /fetch request with missing url', async () => {
326
+ const authContext = { sub: 'test-user' };
327
+ const event = createMockEvent('/fetch', 'POST', {}, authContext);
328
+
329
+ const response = await handler(event, mockContext);
330
+
331
+ expect(response.statusCode).toBe(400);
332
+ const body = JSON.parse(response.body);
333
+ expect(body.error).toContain('url');
334
+ });
335
+
336
+ it('should reject /fetch-structured request with missing schema', async () => {
337
+ const authContext = { sub: 'test-user' };
338
+ const event = createMockEvent(
339
+ '/fetch-structured',
340
+ 'POST',
341
+ { url: 'https://example.com' },
342
+ authContext
343
+ );
344
+
345
+ const response = await handler(event, mockContext);
346
+
347
+ expect(response.statusCode).toBe(400);
348
+ const body = JSON.parse(response.body);
349
+ expect(body.error).toContain('schema');
350
+ });
351
+
352
+ it('should reject invalid JSON body', async () => {
353
+ const event = createMockEvent('/fetch', 'POST', null);
354
+ event.body = '{invalid json}';
355
+
356
+ const response = await handler(event, mockContext);
357
+
358
+ expect(response.statusCode).toBe(400);
359
+ const body = JSON.parse(response.body);
360
+ expect(body.error).toContain('JSON');
361
+ });
362
+ });
363
+
364
+ describe('7. Unknown Endpoint Handling', () => {
365
+ it('should return 404 for unknown paths', async () => {
366
+ const authContext = { sub: 'test-user' };
367
+ const event = createMockEvent('/unknown-endpoint', 'POST', null, authContext);
368
+
369
+ const response = await handler(event, mockContext);
370
+
371
+ expect(response.statusCode).toBe(404);
372
+ const body = JSON.parse(response.body);
373
+ expect(body.error).toContain('Not found');
374
+ });
375
+ });
376
+ });
377
+
378
+ describe('SECURITY AUDIT FINDINGS - RESOLUTIONS', () => {
379
+ it('✅ FINDING 1 RESOLVED: Lambda NOW enforces auth at application level', async () => {
380
+ /**
381
+ * RESOLUTION VERIFIED (v0.3.1):
382
+ * - Lambda handler now validates Cognito authorizer context
383
+ * - Returns 401 if userId is missing (lines 188-209 of lambda-handler.ts)
384
+ * - Logs 'auth_required' event with details
385
+ * - Health check endpoint explicitly excluded from auth requirement
386
+ *
387
+ * FIXED: Application-level defense-in-depth implemented
388
+ */
389
+ const event = createMockEvent('/fetch', 'POST', { url: 'https://example.com' });
390
+ const response = await handler(event, mockContext);
391
+
392
+ // FIXED: Lambda NOW returns 401 when auth is missing
393
+ expect(response.statusCode).toBe(401);
394
+ const body = JSON.parse(response.body);
395
+ expect(body.error).toContain('Unauthorized');
396
+ expect(body.error).toContain('Authentication required');
397
+ });
398
+
399
+ it('✅ FINDING 1 RESOLVED: Auth rejection prevents anonymous audit logs', async () => {
400
+ /**
401
+ * RESOLUTION VERIFIED (v0.3.1):
402
+ * - Unauthenticated requests are rejected before reaching audit logging
403
+ * - No more user_id="anonymous" in audit logs
404
+ * - auth_required event logged instead for security monitoring
405
+ *
406
+ * FIXED: No anonymous audit trails possible
407
+ */
408
+ const event = createMockEvent('/fetch', 'POST', { url: 'https://example.com' });
409
+
410
+ // Intercept console.error to verify auth_required logging
411
+ const originalConsoleError = console.error;
412
+ let authRequiredLogged = false;
413
+ let auditLogAttempted = false;
414
+ console.error = (message: string) => {
415
+ try {
416
+ const parsed = JSON.parse(message);
417
+ if (parsed.event === 'auth_required') {
418
+ authRequiredLogged = true;
419
+ }
420
+ if (parsed.event === 'audit_logging_failed') {
421
+ auditLogAttempted = true;
422
+ }
423
+ } catch {
424
+ // Not JSON
425
+ }
426
+ };
427
+
428
+ await handler(event, mockContext);
429
+
430
+ console.error = originalConsoleError;
431
+
432
+ // Verify auth_required was logged
433
+ expect(authRequiredLogged).toBe(true);
434
+ // Verify NO audit logging attempted (rejected before that point)
435
+ expect(auditLogAttempted).toBe(false);
436
+ });
437
+
438
+ it('✅ FINDING 2 RESOLVED: Health check now supports GET method', async () => {
439
+ /**
440
+ * RESOLUTION VERIFIED (v0.3.1):
441
+ * - Health check moved before POST-only validation (lines 152-165 of lambda-handler.ts)
442
+ * - Supports both GET and POST methods
443
+ * - CORS allows GET, POST, OPTIONS
444
+ * - Standard monitoring tools can now use GET /health
445
+ *
446
+ * FIXED: Standard REST conventions for health checks
447
+ */
448
+ const getEvent = createMockEvent('/health', 'GET', null);
449
+ const getResponse = await handler(getEvent, mockContext);
450
+
451
+ expect(getResponse.statusCode).toBe(200);
452
+ const body = JSON.parse(getResponse.body);
453
+ expect(body.status).toBe('healthy');
454
+ expect(body.version).toBe('0.3.1');
455
+
456
+ // Also verify POST still works
457
+ const postEvent = createMockEvent('/health', 'POST', {});
458
+ const postResponse = await handler(postEvent, mockContext);
459
+ expect(postResponse.statusCode).toBe(200);
460
+ });
461
+
462
+ it('✅ CONFIRMED SECURE: Health check remains intentionally unauthenticated', async () => {
463
+ /**
464
+ * CONFIRMED SECURE (v0.3.1):
465
+ * - /health endpoint intentionally bypasses auth (lines 152-165 of lambda-handler.ts)
466
+ * - This is standard practice for health checks
467
+ * - Only returns non-sensitive metadata (status, version, timestamp)
468
+ *
469
+ * NO ACTION REQUIRED
470
+ */
471
+ const event = createMockEvent('/health', 'GET', null);
472
+ const response = await handler(event, mockContext);
473
+
474
+ expect(response.statusCode).toBe(200);
475
+ const body = JSON.parse(response.body);
476
+ expect(body).not.toHaveProperty('user_id');
477
+ expect(body).not.toHaveProperty('secrets');
478
+ expect(body.status).toBe('healthy');
479
+ });
480
+ });