@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.3

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 (45) hide show
  1. package/README.md +42 -0
  2. package/dist/function/GlobalToolFunction.d.ts +1 -0
  3. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  4. package/dist/function/GlobalToolFunction.js +8 -0
  5. package/dist/function/GlobalToolFunction.js.map +1 -1
  6. package/dist/function/GlobalToolFunction.test.js +3 -0
  7. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  8. package/dist/function/ToolFunction.d.ts +7 -1
  9. package/dist/function/ToolFunction.d.ts.map +1 -1
  10. package/dist/function/ToolFunction.js +14 -1
  11. package/dist/function/ToolFunction.js.map +1 -1
  12. package/dist/function/ToolFunction.test.js +3 -0
  13. package/dist/function/ToolFunction.test.js.map +1 -1
  14. package/dist/logging/ToolLogger.d.ts +34 -0
  15. package/dist/logging/ToolLogger.d.ts.map +1 -0
  16. package/dist/logging/ToolLogger.js +153 -0
  17. package/dist/logging/ToolLogger.js.map +1 -0
  18. package/dist/logging/ToolLogger.test.d.ts +2 -0
  19. package/dist/logging/ToolLogger.test.d.ts.map +1 -0
  20. package/dist/logging/ToolLogger.test.js +646 -0
  21. package/dist/logging/ToolLogger.test.js.map +1 -0
  22. package/dist/service/Service.d.ts.map +1 -1
  23. package/dist/service/Service.js +17 -0
  24. package/dist/service/Service.js.map +1 -1
  25. package/dist/service/Service.test.js +114 -6
  26. package/dist/service/Service.test.js.map +1 -1
  27. package/dist/validation/ParameterValidator.d.ts +42 -0
  28. package/dist/validation/ParameterValidator.d.ts.map +1 -0
  29. package/dist/validation/ParameterValidator.js +122 -0
  30. package/dist/validation/ParameterValidator.js.map +1 -0
  31. package/dist/validation/ParameterValidator.test.d.ts +2 -0
  32. package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
  33. package/dist/validation/ParameterValidator.test.js +282 -0
  34. package/dist/validation/ParameterValidator.test.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/function/GlobalToolFunction.test.ts +3 -0
  37. package/src/function/GlobalToolFunction.ts +11 -0
  38. package/src/function/ToolFunction.test.ts +3 -0
  39. package/src/function/ToolFunction.ts +19 -1
  40. package/src/logging/ToolLogger.test.ts +753 -0
  41. package/src/logging/ToolLogger.ts +177 -0
  42. package/src/service/Service.test.ts +155 -14
  43. package/src/service/Service.ts +19 -1
  44. package/src/validation/ParameterValidator.test.ts +341 -0
  45. package/src/validation/ParameterValidator.ts +153 -0
@@ -0,0 +1,753 @@
1
+ import { ToolLogger } from './ToolLogger';
2
+ import { logger, LogVisibility } from '@zaiusinc/app-sdk';
3
+ import * as App from '@zaiusinc/app-sdk';
4
+
5
+ // Mock the logger
6
+ jest.mock('@zaiusinc/app-sdk', () => ({
7
+ logger: {
8
+ info: jest.fn()
9
+ },
10
+ LogVisibility: {
11
+ Zaius: 'zaius'
12
+ },
13
+ Headers: jest.fn(),
14
+ Response: jest.fn()
15
+ }));
16
+
17
+ describe('ToolLogger', () => {
18
+ const mockLogger = logger as jest.Mocked<typeof logger>;
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ // Helper function to check JSON string logs
25
+ const expectJsonLog = (expectedData: any) => {
26
+ expect(mockLogger.info).toHaveBeenCalledWith(
27
+ LogVisibility.Zaius,
28
+ JSON.stringify(expectedData)
29
+ );
30
+ };
31
+
32
+ const createMockRequest = (overrides: any = {}): App.Request => {
33
+ const defaultRequest = {
34
+ path: '/test-tool',
35
+ bodyJSON: {
36
+ parameters: {
37
+ name: 'test',
38
+ value: 'data'
39
+ }
40
+ },
41
+ headers: {
42
+ get: jest.fn().mockReturnValue('application/json')
43
+ }
44
+ };
45
+
46
+ return { ...defaultRequest, ...overrides };
47
+ };
48
+
49
+ const createMockResponse = (status = 200, bodyJSON: any = {}, headers: any = {}): App.Response => {
50
+ const mockHeaders = {
51
+ get: jest.fn().mockImplementation((name: string) => {
52
+ if (name === 'content-type') return 'application/json';
53
+ return headers[name] || null;
54
+ })
55
+ };
56
+
57
+ const response = {
58
+ status,
59
+ headers: mockHeaders,
60
+ _bodyJSON: bodyJSON
61
+ } as any;
62
+
63
+ // Add getter for bodyJSON that returns the stored value
64
+ Object.defineProperty(response, 'bodyJSON', {
65
+ get() {
66
+ return this._bodyJSON;
67
+ },
68
+ set(value) {
69
+ this._bodyJSON = value;
70
+ },
71
+ enumerable: true
72
+ });
73
+
74
+ // Add getter for bodyAsU8Array that recalculates based on current bodyJSON
75
+ Object.defineProperty(response, 'bodyAsU8Array', {
76
+ get() {
77
+ if (this._bodyJSON !== null && this._bodyJSON !== undefined) {
78
+ // This will throw for circular references, which matches real behavior
79
+ const jsonString = JSON.stringify(this._bodyJSON);
80
+ return new Uint8Array(Buffer.from(jsonString));
81
+ }
82
+ return undefined;
83
+ },
84
+ enumerable: true
85
+ });
86
+
87
+ return response;
88
+ };
89
+
90
+ describe('logRequest', () => {
91
+ it('should log request with parameters', () => {
92
+ const req = createMockRequest();
93
+
94
+ ToolLogger.logRequest(req);
95
+
96
+ const expectedLog = {
97
+ event: 'opal_tool_request',
98
+ path: '/test-tool',
99
+ parameters: {
100
+ name: 'test',
101
+ value: 'data'
102
+ }
103
+ };
104
+
105
+ expect(mockLogger.info).toHaveBeenCalledWith(
106
+ LogVisibility.Zaius,
107
+ JSON.stringify(expectedLog)
108
+ );
109
+ });
110
+
111
+ it('should handle request without parameters', () => {
112
+ const req = createMockRequest({
113
+ bodyJSON: null
114
+ });
115
+
116
+ ToolLogger.logRequest(req);
117
+
118
+ expectJsonLog({
119
+ event: 'opal_tool_request',
120
+ path: '/test-tool',
121
+ parameters: null
122
+ });
123
+ });
124
+
125
+ it('should use bodyJSON as parameters when no parameters field exists', () => {
126
+ const req = createMockRequest({
127
+ bodyJSON: {
128
+ name: 'direct',
129
+ action: 'test'
130
+ }
131
+ });
132
+
133
+ ToolLogger.logRequest(req);
134
+
135
+ expectJsonLog({
136
+ event: 'opal_tool_request',
137
+ path: '/test-tool',
138
+ parameters: {
139
+ name: 'direct',
140
+ action: 'test'
141
+ }
142
+ });
143
+ });
144
+
145
+ it('should redact all sensitive field variations', () => {
146
+ const req = createMockRequest({
147
+ bodyJSON: {
148
+ parameters: {
149
+ username: 'john',
150
+ password: 'secret123',
151
+ api_key: 'key123',
152
+ secret: 'mysecret',
153
+ token: 'abc123',
154
+ auth: 'authdata',
155
+ credentials: 'creds',
156
+ access_token: 'access123',
157
+ refresh_token: 'refresh123',
158
+ private_key: 'privatekey',
159
+ client_secret: 'clientsecret',
160
+ normal_field: 'visible'
161
+ }
162
+ }
163
+ });
164
+
165
+ ToolLogger.logRequest(req);
166
+
167
+ expectJsonLog({
168
+ event: 'opal_tool_request',
169
+ path: '/test-tool',
170
+ parameters: {
171
+ username: 'john',
172
+ password: '[REDACTED]',
173
+ api_key: '[REDACTED]',
174
+ secret: '[REDACTED]',
175
+ token: '[REDACTED]',
176
+ auth: '[REDACTED]',
177
+ credentials: '[REDACTED]',
178
+ access_token: '[REDACTED]',
179
+ refresh_token: '[REDACTED]',
180
+ private_key: '[REDACTED]',
181
+ client_secret: '[REDACTED]',
182
+ normal_field: 'visible'
183
+ }
184
+ });
185
+ });
186
+
187
+ it('should redact sensitive fields with case variations', () => {
188
+ const req = createMockRequest({
189
+ bodyJSON: {
190
+ parameters: {
191
+ PASSWORD: 'secret1',
192
+ API_KEY: 'secret2',
193
+ clientSecret: 'secret3',
194
+ user_password: 'secret4',
195
+ oauth_token: 'secret5',
196
+ ssh_key: 'secret6',
197
+ normal_field: 'visible'
198
+ }
199
+ }
200
+ });
201
+
202
+ ToolLogger.logRequest(req);
203
+
204
+ expectJsonLog({
205
+ event: 'opal_tool_request',
206
+ path: '/test-tool',
207
+ parameters: {
208
+ PASSWORD: '[REDACTED]',
209
+ API_KEY: '[REDACTED]',
210
+ clientSecret: '[REDACTED]',
211
+ user_password: '[REDACTED]',
212
+ oauth_token: '[REDACTED]',
213
+ ssh_key: '[REDACTED]',
214
+ normal_field: 'visible'
215
+ }
216
+ });
217
+ });
218
+
219
+ it('should truncate long string values', () => {
220
+ const longString = 'a'.repeat(150);
221
+ const req = createMockRequest({
222
+ bodyJSON: {
223
+ parameters: {
224
+ description: longString,
225
+ short_field: 'normal'
226
+ }
227
+ }
228
+ });
229
+
230
+ ToolLogger.logRequest(req);
231
+
232
+ expectJsonLog({
233
+ event: 'opal_tool_request',
234
+ path: '/test-tool',
235
+ parameters: {
236
+ description: `${'a'.repeat(100)}... (truncated, 150 chars total)`,
237
+ short_field: 'normal'
238
+ }
239
+ });
240
+ });
241
+
242
+ it('should truncate large arrays', () => {
243
+ const largeArray = Array.from({ length: 15 }, (_, i) => `item${i}`);
244
+ const req = createMockRequest({
245
+ bodyJSON: {
246
+ parameters: {
247
+ items: largeArray,
248
+ small_array: ['a', 'b']
249
+ }
250
+ }
251
+ });
252
+
253
+ ToolLogger.logRequest(req);
254
+
255
+ expectJsonLog({
256
+ event: 'opal_tool_request',
257
+ path: '/test-tool',
258
+ parameters: {
259
+ items: [
260
+ ...largeArray.slice(0, 10),
261
+ '... (5 more items truncated)'
262
+ ],
263
+ small_array: ['a', 'b']
264
+ }
265
+ });
266
+ });
267
+
268
+ it('should handle nested objects with sensitive fields', () => {
269
+ const req = createMockRequest({
270
+ bodyJSON: {
271
+ parameters: {
272
+ user: {
273
+ name: 'John',
274
+ email: 'john@example.com',
275
+ password: 'secret123'
276
+ },
277
+ config: {
278
+ database: {
279
+ host: 'localhost',
280
+ port: 5432,
281
+ password: 'dbpass'
282
+ },
283
+ api_key: 'apikey123'
284
+ }
285
+ }
286
+ }
287
+ });
288
+
289
+ ToolLogger.logRequest(req);
290
+
291
+ expectJsonLog({
292
+ event: 'opal_tool_request',
293
+ path: '/test-tool',
294
+ parameters: {
295
+ user: {
296
+ name: 'John',
297
+ email: '[REDACTED]',
298
+ password: '[REDACTED]'
299
+ },
300
+ config: {
301
+ database: {
302
+ host: 'localhost',
303
+ port: 5432,
304
+ password: '[REDACTED]'
305
+ },
306
+ api_key: '[REDACTED]'
307
+ }
308
+ }
309
+ });
310
+ });
311
+
312
+ it('should handle null and undefined values', () => {
313
+ const req = createMockRequest({
314
+ bodyJSON: {
315
+ parameters: {
316
+ nullValue: null,
317
+ undefinedValue: undefined,
318
+ emptyString: '',
319
+ zero: 0,
320
+ false: false,
321
+ password: null // sensitive field with null value
322
+ }
323
+ }
324
+ });
325
+
326
+ ToolLogger.logRequest(req);
327
+
328
+ expectJsonLog({
329
+ event: 'opal_tool_request',
330
+ path: '/test-tool',
331
+ parameters: {
332
+ nullValue: null,
333
+ emptyString: '',
334
+ zero: 0,
335
+ false: false,
336
+ password: '[REDACTED]'
337
+ }
338
+ });
339
+ });
340
+
341
+ it('should handle arrays in sensitive fields', () => {
342
+ const req = createMockRequest({
343
+ bodyJSON: {
344
+ parameters: {
345
+ credentials: ['user', 'pass', 'token'],
346
+ public_list: ['item1', 'item2']
347
+ }
348
+ }
349
+ });
350
+
351
+ ToolLogger.logRequest(req);
352
+
353
+ expectJsonLog({
354
+ event: 'opal_tool_request',
355
+ path: '/test-tool',
356
+ parameters: {
357
+ credentials: '[REDACTED]',
358
+ public_list: ['item1', 'item2']
359
+ }
360
+ });
361
+ });
362
+
363
+ it('should handle objects in sensitive fields', () => {
364
+ const req = createMockRequest({
365
+ bodyJSON: {
366
+ parameters: {
367
+ auth: {
368
+ username: 'john',
369
+ password: 'secret'
370
+ },
371
+ public_config: {
372
+ timeout: 30,
373
+ retries: 3
374
+ }
375
+ }
376
+ }
377
+ });
378
+
379
+ ToolLogger.logRequest(req);
380
+
381
+ expectJsonLog({
382
+ event: 'opal_tool_request',
383
+ path: '/test-tool',
384
+ parameters: {
385
+ auth: '[REDACTED]',
386
+ public_config: {
387
+ timeout: 30,
388
+ retries: 3
389
+ }
390
+ }
391
+ });
392
+ });
393
+
394
+ it('should respect max depth to prevent infinite recursion', () => {
395
+ const deepObject: any = { level: 0, data: 'test' };
396
+ let current = deepObject;
397
+
398
+ // Create a very deep nested object (deeper than maxDepth)
399
+ for (let i = 1; i <= 10; i++) {
400
+ current.nested = { level: i, data: `level${i}` };
401
+ current = current.nested;
402
+ }
403
+
404
+ const req = createMockRequest({
405
+ bodyJSON: { parameters: { deep: deepObject } }
406
+ });
407
+
408
+ // Should not throw error or cause infinite recursion
409
+ expect(() => ToolLogger.logRequest(req)).not.toThrow();
410
+ expect(mockLogger.info).toHaveBeenCalled();
411
+ });
412
+
413
+ it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
414
+ // Create an object with exactly 6 levels (exceeds maxDepth of 5)
415
+ const deepObject = {
416
+ level1: {
417
+ level2: {
418
+ level3: {
419
+ level4: {
420
+ level5: {
421
+ level6: {
422
+ password: 'should-not-be-visible',
423
+ credit_card: '1234-5678-9012-3456',
424
+ data: 'sensitive-info'
425
+ }
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ };
432
+
433
+ const req = createMockRequest({
434
+ bodyJSON: { parameters: { deep: deepObject } }
435
+ });
436
+
437
+ ToolLogger.logRequest(req);
438
+
439
+ // Verify that the deeply nested object is replaced with placeholder
440
+ const logCall = mockLogger.info.mock.calls[0];
441
+ const loggedData = JSON.parse(logCall[1]);
442
+
443
+ // Navigate to the deeply nested part that should be replaced
444
+ // At maxDepth=5, the 5th level (level4) gets replaced with the placeholder
445
+ const level4 = loggedData.parameters.deep.level1.level2.level3.level4;
446
+ expect(level4).toBe('[MAX_DEPTH_EXCEEDED]');
447
+ });
448
+
449
+ it('should handle arrays containing deeply nested objects that exceed max depth', () => {
450
+ // Create a structure where the array is shallow enough to be processed (depth 3),
451
+ // but individual objects within the array exceed the max depth
452
+ const arrayWithDeepObjects = {
453
+ level1: {
454
+ items: [
455
+ {
456
+ level2: {
457
+ level3: {
458
+ level4: {
459
+ level5: {
460
+ password: 'secret-in-deep-array-object',
461
+ credit_card: '1234-5678-9012-3456'
462
+ }
463
+ }
464
+ }
465
+ }
466
+ },
467
+ 'simple-item',
468
+ {
469
+ shallow: 'data'
470
+ },
471
+ {
472
+ level2: {
473
+ level3: {
474
+ level4: {
475
+ another: 'deep-object'
476
+ }
477
+ }
478
+ }
479
+ }
480
+ ]
481
+ }
482
+ };
483
+
484
+ const req = createMockRequest({
485
+ bodyJSON: { parameters: arrayWithDeepObjects }
486
+ });
487
+
488
+ ToolLogger.logRequest(req);
489
+
490
+ // Verify the array structure and depth handling
491
+ const logCall = mockLogger.info.mock.calls[0];
492
+ const loggedData = JSON.parse(logCall[1]);
493
+
494
+ const items = loggedData.parameters.level1.items;
495
+
496
+ // First item: deeply nested object with inner parts replaced by placeholder
497
+ expect(items[0]).toEqual({
498
+ level2: {
499
+ level3: '[MAX_DEPTH_EXCEEDED]'
500
+ }
501
+ });
502
+
503
+ // Second item: simple string should remain unchanged
504
+ expect(items[1]).toBe('simple-item');
505
+
506
+ // Third item: shallow object should be processed normally
507
+ expect(items[2]).toEqual({ shallow: 'data' });
508
+
509
+ // Fourth item: another deeply nested object with inner parts replaced by placeholder
510
+ expect(items[3]).toEqual({
511
+ level2: {
512
+ level3: '[MAX_DEPTH_EXCEEDED]'
513
+ }
514
+ });
515
+ });
516
+ });
517
+
518
+ describe('logResponse', () => {
519
+ it('should log successful response with all details', () => {
520
+ const req = createMockRequest();
521
+ const response = createMockResponse(200, { result: 'success', data: 'test' });
522
+
523
+ ToolLogger.logResponse(req, response, 150);
524
+
525
+ const expectedLog = {
526
+ event: 'opal_tool_response',
527
+ path: '/test-tool',
528
+ duration: '150ms',
529
+ status: 200,
530
+ contentType: 'application/json',
531
+ contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
532
+ success: true
533
+ };
534
+
535
+ expect(mockLogger.info).toHaveBeenCalledWith(
536
+ LogVisibility.Zaius,
537
+ JSON.stringify(expectedLog)
538
+ );
539
+ });
540
+
541
+ it('should log error response', () => {
542
+ const req = createMockRequest();
543
+ const response = createMockResponse(400, { error: 'Bad request' });
544
+
545
+ ToolLogger.logResponse(req, response, 75);
546
+
547
+ expectJsonLog({
548
+ event: 'opal_tool_response',
549
+ path: '/test-tool',
550
+ duration: '75ms',
551
+ status: 400,
552
+ contentType: 'application/json',
553
+ contentLength: 23,
554
+ success: false
555
+ });
556
+ });
557
+
558
+ it('should handle response without bodyJSON', () => {
559
+ const req = createMockRequest();
560
+ const response = createMockResponse(204);
561
+ response.bodyJSON = undefined;
562
+
563
+ ToolLogger.logResponse(req, response);
564
+
565
+ expectJsonLog({
566
+ event: 'opal_tool_response',
567
+ path: '/test-tool',
568
+ status: 204,
569
+ contentType: 'application/json',
570
+ contentLength: 'unknown',
571
+ success: true
572
+ });
573
+ });
574
+
575
+ it('should handle response without processing time', () => {
576
+ const req = createMockRequest();
577
+ const response = createMockResponse(200, { data: 'test' });
578
+
579
+ ToolLogger.logResponse(req, response);
580
+
581
+ expectJsonLog({
582
+ event: 'opal_tool_response',
583
+ path: '/test-tool',
584
+ status: 200,
585
+ contentType: 'application/json',
586
+ contentLength: 15,
587
+ success: true
588
+ });
589
+ });
590
+
591
+ it('should handle unknown content type', () => {
592
+ const req = createMockRequest();
593
+ const response = createMockResponse(200, { data: 'test' });
594
+ response.headers.get = jest.fn().mockReturnValue(null);
595
+
596
+ ToolLogger.logResponse(req, response);
597
+
598
+ expectJsonLog({
599
+ event: 'opal_tool_response',
600
+ path: '/test-tool',
601
+ status: 200,
602
+ contentType: 'unknown',
603
+ contentLength: 15,
604
+ success: true
605
+ });
606
+ });
607
+
608
+ it('should handle content length calculation error', () => {
609
+ const req = createMockRequest();
610
+ const circularObj: any = { name: 'test' };
611
+ circularObj.self = circularObj; // Create circular reference
612
+
613
+ const response = createMockResponse(200, circularObj);
614
+
615
+ ToolLogger.logResponse(req, response);
616
+
617
+ expectJsonLog({
618
+ event: 'opal_tool_response',
619
+ path: '/test-tool',
620
+ status: 200,
621
+ contentType: 'application/json',
622
+ contentLength: 'unknown',
623
+ success: true
624
+ });
625
+ });
626
+
627
+ it('should correctly identify success status codes', () => {
628
+ const req = createMockRequest();
629
+
630
+ const testCases = [
631
+ { status: 200, expected: true },
632
+ { status: 201, expected: true },
633
+ { status: 204, expected: true },
634
+ { status: 299, expected: true },
635
+ { status: 300, expected: false },
636
+ { status: 400, expected: false },
637
+ { status: 404, expected: false },
638
+ { status: 500, expected: false }
639
+ ];
640
+
641
+ testCases.forEach(({ status, expected }) => {
642
+ mockLogger.info.mockClear();
643
+ const response = createMockResponse(status);
644
+ ToolLogger.logResponse(req, response);
645
+
646
+ expectJsonLog({
647
+ event: 'opal_tool_response',
648
+ path: '/test-tool',
649
+ status,
650
+ contentType: 'application/json',
651
+ contentLength: 2,
652
+ success: expected
653
+ });
654
+ });
655
+ });
656
+
657
+ it('should handle different content types', () => {
658
+ const req = createMockRequest();
659
+
660
+ const testCases = [
661
+ 'application/json',
662
+ 'text/plain',
663
+ 'application/xml',
664
+ 'text/html'
665
+ ];
666
+
667
+ testCases.forEach((contentType) => {
668
+ mockLogger.info.mockClear();
669
+ const response = createMockResponse(200, { data: 'test' });
670
+ response.headers.get = jest.fn().mockReturnValue(contentType);
671
+
672
+ ToolLogger.logResponse(req, response);
673
+
674
+ expectJsonLog({
675
+ event: 'opal_tool_response',
676
+ path: '/test-tool',
677
+ status: 200,
678
+ contentType,
679
+ contentLength: 15,
680
+ success: true
681
+ });
682
+ });
683
+ });
684
+ });
685
+
686
+ describe('edge cases', () => {
687
+ it('should handle empty request bodyJSON', () => {
688
+ const req = createMockRequest({
689
+ bodyJSON: {}
690
+ });
691
+
692
+ ToolLogger.logRequest(req);
693
+
694
+ expectJsonLog({
695
+ event: 'opal_tool_request',
696
+ path: '/test-tool',
697
+ parameters: {}
698
+ });
699
+ });
700
+
701
+ it('should handle request with only parameters field', () => {
702
+ const req = createMockRequest({
703
+ bodyJSON: {
704
+ parameters: {
705
+ field: 'value' // Changed from 'key' to 'field' to avoid sensitive field detection
706
+ }
707
+ }
708
+ });
709
+
710
+ ToolLogger.logRequest(req);
711
+
712
+ expectJsonLog({
713
+ event: 'opal_tool_request',
714
+ path: '/test-tool',
715
+ parameters: {
716
+ field: 'value'
717
+ }
718
+ });
719
+ });
720
+
721
+ it('should handle mixed data types in parameters', () => {
722
+ const req = createMockRequest({
723
+ bodyJSON: {
724
+ parameters: {
725
+ string: 'text',
726
+ number: 42,
727
+ boolean: true,
728
+ array: [1, 2, 3],
729
+ object: { nested: 'value' },
730
+ nullValue: null,
731
+ password: 'secret'
732
+ }
733
+ }
734
+ });
735
+
736
+ ToolLogger.logRequest(req);
737
+
738
+ expectJsonLog({
739
+ event: 'opal_tool_request',
740
+ path: '/test-tool',
741
+ parameters: {
742
+ string: 'text',
743
+ number: 42,
744
+ boolean: true,
745
+ array: [1, 2, 3],
746
+ object: { nested: 'value' },
747
+ nullValue: null,
748
+ password: '[REDACTED]'
749
+ }
750
+ });
751
+ });
752
+ });
753
+ });