@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.9 → 1.1.0-beta.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.
- package/README.md +99 -200
- package/dist/function/GlobalToolFunction.d.ts +4 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +25 -19
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +114 -193
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +4 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +20 -21
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +73 -263
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +11 -3
- package/dist/logging/ToolLogger.d.ts.map +1 -1
- package/dist/logging/ToolLogger.js +114 -13
- package/dist/logging/ToolLogger.js.map +1 -1
- package/dist/logging/ToolLogger.test.js +177 -71
- package/dist/logging/ToolLogger.test.js.map +1 -1
- package/dist/service/Service.d.ts +10 -9
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +42 -74
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +60 -95
- package/dist/service/Service.test.js.map +1 -1
- package/dist/utils/ErrorFormatter.d.ts +9 -0
- package/dist/utils/ErrorFormatter.d.ts.map +1 -0
- package/dist/utils/ErrorFormatter.js +25 -0
- package/dist/utils/ErrorFormatter.js.map +1 -0
- package/package.json +3 -3
- package/src/function/GlobalToolFunction.test.ts +113 -213
- package/src/function/GlobalToolFunction.ts +29 -29
- package/src/function/ToolFunction.test.ts +78 -285
- package/src/function/ToolFunction.ts +24 -30
- package/src/logging/ToolLogger.test.ts +225 -74
- package/src/logging/ToolLogger.ts +129 -15
- package/src/service/Service.test.ts +61 -113
- package/src/service/Service.ts +45 -79
- package/src/utils/ErrorFormatter.ts +31 -0
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ToolLogger = void 0;
|
|
4
4
|
const app_sdk_1 = require("@zaiusinc/app-sdk");
|
|
5
|
+
const MAX_PARAM_LOG_LENGTH = 128;
|
|
6
|
+
const MAX_BODY_LOG_LENGTH = 256;
|
|
7
|
+
const MAX_ARRAY_ITEMS = 2;
|
|
5
8
|
/**
|
|
6
9
|
* Utility class for logging Opal tool requests and responses with security considerations
|
|
7
10
|
*/
|
|
@@ -45,12 +48,13 @@ class ToolLogger {
|
|
|
45
48
|
'jwt',
|
|
46
49
|
'bearer_token'
|
|
47
50
|
];
|
|
48
|
-
static MAX_PARAM_LENGTH = 100;
|
|
49
|
-
static MAX_ARRAY_ITEMS = 10;
|
|
50
51
|
/**
|
|
51
52
|
* Redacts sensitive data from an object
|
|
52
53
|
*/
|
|
53
|
-
static
|
|
54
|
+
static redactSensitiveDataAndTruncate(data, maxDepth = 5, accumulatedLength = 0) {
|
|
55
|
+
if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
54
58
|
if (maxDepth <= 0) {
|
|
55
59
|
return '[MAX_DEPTH_EXCEEDED]';
|
|
56
60
|
}
|
|
@@ -58,31 +62,42 @@ class ToolLogger {
|
|
|
58
62
|
return data;
|
|
59
63
|
}
|
|
60
64
|
if (typeof data === 'string') {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
if (data.length > MAX_PARAM_LOG_LENGTH) {
|
|
66
|
+
const lead = data.substring(0, MAX_PARAM_LOG_LENGTH - 10);
|
|
67
|
+
const tail = data.substring(data.length - 10);
|
|
68
|
+
return `${lead}...[${data.length - MAX_PARAM_LOG_LENGTH} truncated]...${tail}`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
if (typeof data === 'number' || typeof data === 'boolean') {
|
|
66
75
|
return data;
|
|
67
76
|
}
|
|
68
77
|
if (Array.isArray(data)) {
|
|
69
|
-
const truncated = data.slice(0,
|
|
70
|
-
const result = truncated.map((item) => this.
|
|
71
|
-
if (data.length >
|
|
72
|
-
result.push(`... (${data.length -
|
|
78
|
+
const truncated = data.slice(0, MAX_ARRAY_ITEMS);
|
|
79
|
+
const result = truncated.map((item) => this.redactSensitiveDataAndTruncate(item, maxDepth, accumulatedLength));
|
|
80
|
+
if (data.length > MAX_ARRAY_ITEMS) {
|
|
81
|
+
result.push(`... (${data.length - MAX_ARRAY_ITEMS} more items truncated)`);
|
|
73
82
|
}
|
|
74
83
|
return result;
|
|
75
84
|
}
|
|
76
85
|
if (typeof data === 'object') {
|
|
77
86
|
const result = {};
|
|
78
87
|
for (const [key, value] of Object.entries(data)) {
|
|
88
|
+
if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
79
91
|
// Check if this field contains sensitive data
|
|
80
92
|
const isSensitive = this.isSensitiveField(key);
|
|
81
93
|
if (isSensitive) {
|
|
82
94
|
result[key] = '[REDACTED]';
|
|
83
95
|
}
|
|
84
96
|
else {
|
|
85
|
-
result[key] = this.
|
|
97
|
+
result[key] = this.redactSensitiveDataAndTruncate(value, maxDepth - 1, accumulatedLength);
|
|
98
|
+
}
|
|
99
|
+
if (result[key]) {
|
|
100
|
+
accumulatedLength += JSON.stringify(result[key]).length;
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
103
|
return result;
|
|
@@ -103,7 +118,7 @@ class ToolLogger {
|
|
|
103
118
|
if (!params) {
|
|
104
119
|
return null;
|
|
105
120
|
}
|
|
106
|
-
return this.
|
|
121
|
+
return this.redactSensitiveDataAndTruncate(params);
|
|
107
122
|
}
|
|
108
123
|
/**
|
|
109
124
|
* Calculates content length of response data
|
|
@@ -119,6 +134,87 @@ class ToolLogger {
|
|
|
119
134
|
return 'unknown';
|
|
120
135
|
}
|
|
121
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Extracts the response body as a string or parsed JSON object
|
|
139
|
+
*/
|
|
140
|
+
static getResponseBody(response) {
|
|
141
|
+
if (!response) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const contentType = response.headers?.get('content-type') || '';
|
|
146
|
+
const isJson = contentType.includes('application/json') || contentType.includes('application/problem+json');
|
|
147
|
+
const isText = contentType.startsWith('text/');
|
|
148
|
+
if (!isJson && !isText) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
// Try to access bodyAsU8Array - this may throw
|
|
152
|
+
const bodyData = response.bodyAsU8Array;
|
|
153
|
+
if (!bodyData) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// Convert Uint8Array to string
|
|
157
|
+
const bodyString = Buffer.from(bodyData).toString();
|
|
158
|
+
if (!bodyString) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// Try to parse as JSON if content-type indicates JSON
|
|
162
|
+
if (isJson) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(bodyString);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// If JSON parsing fails, return as string
|
|
168
|
+
return bodyString;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Return as plain text for non-JSON content types
|
|
172
|
+
return bodyString;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Creates a summary of response body with security redaction and truncation
|
|
180
|
+
* For failed responses (4xx, 5xx): returns full body with redacted sensitive data
|
|
181
|
+
* For successful responses (2xx): returns first 100 chars with redacted sensitive data
|
|
182
|
+
*/
|
|
183
|
+
static createResponseBodySummary(response, success) {
|
|
184
|
+
const body = this.getResponseBody(response);
|
|
185
|
+
if (body === null || body === undefined) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
// For objects (parsed JSON), apply redaction
|
|
189
|
+
if (typeof body === 'object') {
|
|
190
|
+
// For failed responses, don't truncate strings within the object
|
|
191
|
+
const redactedBody = this.redactSensitiveDataAndTruncate(body, 5);
|
|
192
|
+
// For successful responses, truncate to first MAX_BODY_LOG_LENGTH chars
|
|
193
|
+
if (success) {
|
|
194
|
+
const bodyString = JSON.stringify(redactedBody);
|
|
195
|
+
if (bodyString.length > MAX_BODY_LOG_LENGTH) {
|
|
196
|
+
const truncated = bodyString.substring(0, MAX_BODY_LOG_LENGTH);
|
|
197
|
+
return `${truncated}... (truncated)`;
|
|
198
|
+
}
|
|
199
|
+
return redactedBody;
|
|
200
|
+
}
|
|
201
|
+
// For failed responses, return full redacted body
|
|
202
|
+
return redactedBody;
|
|
203
|
+
}
|
|
204
|
+
// For strings (plain text or unparseable JSON)
|
|
205
|
+
if (typeof body === 'string') {
|
|
206
|
+
// For successful responses, truncate to first 100 chars
|
|
207
|
+
if (success) {
|
|
208
|
+
if (body.length > MAX_BODY_LOG_LENGTH) {
|
|
209
|
+
return `${body.substring(0, MAX_BODY_LOG_LENGTH)}... (truncated)`;
|
|
210
|
+
}
|
|
211
|
+
return body;
|
|
212
|
+
}
|
|
213
|
+
// For failed responses, return full body
|
|
214
|
+
return body;
|
|
215
|
+
}
|
|
216
|
+
return body;
|
|
217
|
+
}
|
|
122
218
|
/**
|
|
123
219
|
* Logs an incoming request
|
|
124
220
|
*/
|
|
@@ -137,6 +233,7 @@ class ToolLogger {
|
|
|
137
233
|
* Logs a successful response
|
|
138
234
|
*/
|
|
139
235
|
static logResponse(req, response, processingTimeMs) {
|
|
236
|
+
const success = response.status >= 200 && response.status < 300;
|
|
140
237
|
const responseLog = {
|
|
141
238
|
event: 'opal_tool_response',
|
|
142
239
|
path: req.path,
|
|
@@ -144,8 +241,12 @@ class ToolLogger {
|
|
|
144
241
|
status: response.status,
|
|
145
242
|
contentType: response.headers?.get('content-type') || 'unknown',
|
|
146
243
|
contentLength: this.calculateContentLength(response),
|
|
147
|
-
success
|
|
244
|
+
success
|
|
148
245
|
};
|
|
246
|
+
const responseBodySummary = this.createResponseBodySummary(response, success);
|
|
247
|
+
if (responseBodySummary) {
|
|
248
|
+
responseLog.responseBody = responseBodySummary;
|
|
249
|
+
}
|
|
149
250
|
// Log with Zaius audience so developers only see requests for accounts they have access to
|
|
150
251
|
app_sdk_1.logger.info(app_sdk_1.LogVisibility.Zaius, JSON.stringify(responseLog));
|
|
151
252
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolLogger.js","sourceRoot":"","sources":["../../src/logging/ToolLogger.ts"],"names":[],"mappings":";;;AAAA,+CAA0D;AAG1D;;GAEG;AACH,MAAa,UAAU;IACb,MAAM,CAAU,gBAAgB,GAAG;QACzC,2BAA2B;QAC3B,UAAU;QACV,MAAM;QACN,QAAQ;QACR,KAAK;QACL,OAAO;QACP,MAAM;QACN,aAAa;QACb,cAAc;QACd,eAAe;QACf,SAAS;QACT,aAAa;QACb,eAAe;QACf,eAAe;QACf,eAAe;QAEf,kBAAkB;QAClB,aAAa;QACb,aAAa;QACb,KAAK;QACL,aAAa;QAEb,gBAAgB;QAChB,KAAK,EAAE,yBAAyB;QAChC,KAAK,EAAE,cAAc;QACrB,UAAU;QACV,KAAK,EAAE,gBAAgB;QACvB,OAAO;QACP,OAAO;QACP,SAAS;QAET,qBAAqB;QACrB,KAAK;QACL,KAAK;QACL,iBAAiB;QACjB,mBAAmB;QACnB,aAAa;QACb,gBAAgB;QAChB,KAAK;QACL,cAAc;KACf,CAAC;
|
|
1
|
+
{"version":3,"file":"ToolLogger.js","sourceRoot":"","sources":["../../src/logging/ToolLogger.ts"],"names":[],"mappings":"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"}
|
|
@@ -75,6 +75,22 @@ describe('ToolLogger', () => {
|
|
|
75
75
|
});
|
|
76
76
|
return response;
|
|
77
77
|
};
|
|
78
|
+
const createMockResponseWithBody = (status, bodyData, contentType) => {
|
|
79
|
+
const mockHeaders = {
|
|
80
|
+
get: jest.fn().mockReturnValue(contentType)
|
|
81
|
+
};
|
|
82
|
+
const response = {
|
|
83
|
+
status,
|
|
84
|
+
headers: mockHeaders
|
|
85
|
+
};
|
|
86
|
+
Object.defineProperty(response, 'bodyAsU8Array', {
|
|
87
|
+
get() {
|
|
88
|
+
return bodyData;
|
|
89
|
+
},
|
|
90
|
+
enumerable: true
|
|
91
|
+
});
|
|
92
|
+
return response;
|
|
93
|
+
};
|
|
78
94
|
describe('logRequest', () => {
|
|
79
95
|
it('should log request with parameters', () => {
|
|
80
96
|
const req = createMockRequest();
|
|
@@ -206,7 +222,7 @@ describe('ToolLogger', () => {
|
|
|
206
222
|
path: '/test-tool',
|
|
207
223
|
method: 'POST',
|
|
208
224
|
parameters: {
|
|
209
|
-
description: `${'a'.repeat(
|
|
225
|
+
description: `${'a'.repeat(118)}...[22 truncated]...${'a'.repeat(10)}`,
|
|
210
226
|
short_field: 'normal'
|
|
211
227
|
}
|
|
212
228
|
});
|
|
@@ -228,8 +244,8 @@ describe('ToolLogger', () => {
|
|
|
228
244
|
method: 'POST',
|
|
229
245
|
parameters: {
|
|
230
246
|
items: [
|
|
231
|
-
...largeArray.slice(0,
|
|
232
|
-
'... (
|
|
247
|
+
...largeArray.slice(0, 2),
|
|
248
|
+
'... (13 more items truncated)'
|
|
233
249
|
],
|
|
234
250
|
small_array: ['a', 'b']
|
|
235
251
|
}
|
|
@@ -367,6 +383,19 @@ describe('ToolLogger', () => {
|
|
|
367
383
|
// Should not throw error or cause infinite recursion
|
|
368
384
|
expect(() => ToolLogger_1.ToolLogger.logRequest(req)).not.toThrow();
|
|
369
385
|
expect(mockLogger.info).toHaveBeenCalled();
|
|
386
|
+
// Verify that deeply nested parts are replaced with placeholder
|
|
387
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
388
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
389
|
+
// Navigate to a deeply nested level that should be truncated
|
|
390
|
+
// At maxDepth=5, objects beyond depth 5 should be replaced
|
|
391
|
+
const nested1 = loggedData.parameters.deep.nested;
|
|
392
|
+
expect(nested1).toBeDefined(); // depth 2
|
|
393
|
+
const nested2 = nested1.nested;
|
|
394
|
+
expect(nested2).toBeDefined(); // depth 3
|
|
395
|
+
const nested3 = nested2.nested;
|
|
396
|
+
expect(nested3).toBeDefined(); // depth 4
|
|
397
|
+
const nested4 = nested3.nested;
|
|
398
|
+
expect(nested4).toBe('[MAX_DEPTH_EXCEEDED]'); // depth 5, should be truncated
|
|
370
399
|
});
|
|
371
400
|
it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
|
|
372
401
|
// Create an object with exactly 6 levels (exceeds maxDepth of 5)
|
|
@@ -406,6 +435,7 @@ describe('ToolLogger', () => {
|
|
|
406
435
|
level1: {
|
|
407
436
|
items: [
|
|
408
437
|
{
|
|
438
|
+
shallow: 'data',
|
|
409
439
|
level2: {
|
|
410
440
|
level3: {
|
|
411
441
|
level4: {
|
|
@@ -441,8 +471,11 @@ describe('ToolLogger', () => {
|
|
|
441
471
|
const logCall = mockLogger.info.mock.calls[0];
|
|
442
472
|
const loggedData = JSON.parse(logCall[1]);
|
|
443
473
|
const items = loggedData.parameters.level1.items;
|
|
474
|
+
expect(items.length).toBe(3);
|
|
444
475
|
// First item: deeply nested object with inner parts replaced by placeholder
|
|
476
|
+
// shallow object should be processed normally
|
|
445
477
|
expect(items[0]).toEqual({
|
|
478
|
+
shallow: 'data',
|
|
446
479
|
level2: {
|
|
447
480
|
level3: {
|
|
448
481
|
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
@@ -451,16 +484,6 @@ describe('ToolLogger', () => {
|
|
|
451
484
|
});
|
|
452
485
|
// Second item: simple string should remain unchanged
|
|
453
486
|
expect(items[1]).toBe('simple-item');
|
|
454
|
-
// Third item: shallow object should be processed normally
|
|
455
|
-
expect(items[2]).toEqual({ shallow: 'data' });
|
|
456
|
-
// Fourth item: another deeply nested object with inner parts replaced by placeholder
|
|
457
|
-
expect(items[3]).toEqual({
|
|
458
|
-
level2: {
|
|
459
|
-
level3: {
|
|
460
|
-
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
487
|
});
|
|
465
488
|
});
|
|
466
489
|
describe('logResponse', () => {
|
|
@@ -475,7 +498,8 @@ describe('ToolLogger', () => {
|
|
|
475
498
|
status: 200,
|
|
476
499
|
contentType: 'application/json',
|
|
477
500
|
contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
|
|
478
|
-
success: true
|
|
501
|
+
success: true,
|
|
502
|
+
responseBody: { result: 'success', data: 'test' }
|
|
479
503
|
};
|
|
480
504
|
expect(mockLogger.info).toHaveBeenCalledWith(app_sdk_1.LogVisibility.Zaius, JSON.stringify(expectedLog));
|
|
481
505
|
});
|
|
@@ -490,13 +514,13 @@ describe('ToolLogger', () => {
|
|
|
490
514
|
status: 400,
|
|
491
515
|
contentType: 'application/json',
|
|
492
516
|
contentLength: 23,
|
|
493
|
-
success: false
|
|
517
|
+
success: false,
|
|
518
|
+
responseBody: { error: 'Bad request' }
|
|
494
519
|
});
|
|
495
520
|
});
|
|
496
|
-
it('should handle response without
|
|
521
|
+
it('should handle response without body data', () => {
|
|
497
522
|
const req = createMockRequest();
|
|
498
|
-
const response =
|
|
499
|
-
response.bodyJSON = undefined;
|
|
523
|
+
const response = createMockResponseWithBody(204, undefined, 'application/json');
|
|
500
524
|
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
501
525
|
expectJsonLog({
|
|
502
526
|
event: 'opal_tool_response',
|
|
@@ -517,10 +541,11 @@ describe('ToolLogger', () => {
|
|
|
517
541
|
status: 200,
|
|
518
542
|
contentType: 'application/json',
|
|
519
543
|
contentLength: 15,
|
|
520
|
-
success: true
|
|
544
|
+
success: true,
|
|
545
|
+
responseBody: { data: 'test' }
|
|
521
546
|
});
|
|
522
547
|
});
|
|
523
|
-
it('should handle unknown content type', () => {
|
|
548
|
+
it('should handle unknown content type - response body not logged', () => {
|
|
524
549
|
const req = createMockRequest();
|
|
525
550
|
const response = createMockResponse(200, { data: 'test' });
|
|
526
551
|
response.headers.get = jest.fn().mockReturnValue(null);
|
|
@@ -536,18 +561,29 @@ describe('ToolLogger', () => {
|
|
|
536
561
|
});
|
|
537
562
|
it('should handle content length calculation error', () => {
|
|
538
563
|
const req = createMockRequest();
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
path: '/test-tool',
|
|
564
|
+
// Simulate a response that will cause errors when trying to calculate content length
|
|
565
|
+
// by providing a Uint8Array but the underlying data causes issues
|
|
566
|
+
const mockHeaders = {
|
|
567
|
+
get: jest.fn().mockReturnValue('application/json')
|
|
568
|
+
};
|
|
569
|
+
const response = {
|
|
546
570
|
status: 200,
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
571
|
+
headers: mockHeaders
|
|
572
|
+
};
|
|
573
|
+
// Create a getter that throws when accessed (simulating serialization error)
|
|
574
|
+
Object.defineProperty(response, 'bodyAsU8Array', {
|
|
575
|
+
get() {
|
|
576
|
+
throw new Error('Circular structure');
|
|
577
|
+
},
|
|
578
|
+
enumerable: true
|
|
550
579
|
});
|
|
580
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
581
|
+
// The error causes both contentLength and responseBody to fail gracefully
|
|
582
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
583
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
584
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
585
|
+
expect(loggedData.contentLength).toBe('unknown');
|
|
586
|
+
expect(loggedData.responseBody).toBeUndefined();
|
|
551
587
|
});
|
|
552
588
|
it('should correctly identify success status codes', () => {
|
|
553
589
|
const req = createMockRequest();
|
|
@@ -565,38 +601,125 @@ describe('ToolLogger', () => {
|
|
|
565
601
|
mockLogger.info.mockClear();
|
|
566
602
|
const response = createMockResponse(status);
|
|
567
603
|
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
604
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
605
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
606
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
607
|
+
expect(loggedData.path).toBe('/test-tool');
|
|
608
|
+
expect(loggedData.status).toBe(status);
|
|
609
|
+
expect(loggedData.contentType).toBe('application/json');
|
|
610
|
+
expect(loggedData.contentLength).toBe(2);
|
|
611
|
+
expect(loggedData.success).toBe(expected);
|
|
612
|
+
expect(loggedData.responseBody).toEqual({});
|
|
576
613
|
});
|
|
577
614
|
});
|
|
578
615
|
it('should handle different content types', () => {
|
|
579
616
|
const req = createMockRequest();
|
|
580
617
|
const testCases = [
|
|
581
|
-
'application/json',
|
|
582
|
-
'text/plain',
|
|
583
|
-
'
|
|
584
|
-
'text/html'
|
|
618
|
+
{ contentType: 'application/json', expectedBody: { data: 'test' } },
|
|
619
|
+
{ contentType: 'text/plain', expectedBody: '{"data":"test"}' },
|
|
620
|
+
{ contentType: 'text/html', expectedBody: '{"data":"test"}' }
|
|
585
621
|
];
|
|
586
|
-
testCases.forEach((contentType) => {
|
|
622
|
+
testCases.forEach(({ contentType, expectedBody }) => {
|
|
587
623
|
mockLogger.info.mockClear();
|
|
588
624
|
const response = createMockResponse(200, { data: 'test' });
|
|
589
625
|
response.headers.get = jest.fn().mockReturnValue(contentType);
|
|
590
626
|
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
627
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
628
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
629
|
+
expect(loggedData.event).toBe('opal_tool_response');
|
|
630
|
+
expect(loggedData.path).toBe('/test-tool');
|
|
631
|
+
expect(loggedData.status).toBe(200);
|
|
632
|
+
expect(loggedData.contentType).toBe(contentType);
|
|
633
|
+
expect(loggedData.contentLength).toBe(15);
|
|
634
|
+
expect(loggedData.success).toBe(true);
|
|
635
|
+
expect(loggedData.responseBody).toEqual(expectedBody);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
it('should log short successful response body', () => {
|
|
639
|
+
const req = createMockRequest();
|
|
640
|
+
const response = createMockResponse(200, { result: 'success', data: 'test' });
|
|
641
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
642
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
643
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
644
|
+
expect(loggedData.responseBody).toEqual({ result: 'success', data: 'test' });
|
|
645
|
+
expect(loggedData.success).toBe(true);
|
|
646
|
+
});
|
|
647
|
+
it('should truncate long successful response body to 256 chars', () => {
|
|
648
|
+
const req = createMockRequest();
|
|
649
|
+
const longData = 'a'.repeat(300);
|
|
650
|
+
const response = createMockResponse(200, { message: longData });
|
|
651
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
652
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
653
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
654
|
+
// The response body should be truncated when stringified
|
|
655
|
+
expect(loggedData.responseBody.message).toContain(' truncated]...');
|
|
656
|
+
});
|
|
657
|
+
it('truncates long properties of failed responses', () => {
|
|
658
|
+
const req = createMockRequest();
|
|
659
|
+
const longData = 'a'.repeat(150);
|
|
660
|
+
const response = createMockResponse(400, { error: 'Bad request', details: longData });
|
|
661
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
662
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
663
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
664
|
+
// Failed responses should include full body, not truncated
|
|
665
|
+
expect(loggedData.responseBody.error).toEqual('Bad request');
|
|
666
|
+
expect(loggedData.responseBody.details).toContain(' truncated]...');
|
|
667
|
+
expect(loggedData.success).toBe(false);
|
|
668
|
+
});
|
|
669
|
+
it('should redact sensitive data in response body', () => {
|
|
670
|
+
const req = createMockRequest();
|
|
671
|
+
const response = createMockResponse(200, {
|
|
672
|
+
user: 'john',
|
|
673
|
+
password: 'secret123',
|
|
674
|
+
api_key: 'key456',
|
|
675
|
+
data: 'public'
|
|
599
676
|
});
|
|
677
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
678
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
679
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
680
|
+
expect(loggedData.responseBody).toEqual({
|
|
681
|
+
user: 'john',
|
|
682
|
+
password: '[REDACTED]',
|
|
683
|
+
api_key: '[REDACTED]',
|
|
684
|
+
data: 'public'
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
it('should handle response with no body', () => {
|
|
688
|
+
const req = createMockRequest();
|
|
689
|
+
const response = createMockResponseWithBody(204, undefined, 'application/json');
|
|
690
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
691
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
692
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
693
|
+
expect(loggedData.responseBody).toBeUndefined();
|
|
694
|
+
expect(loggedData.success).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
it('should handle plain text response body', () => {
|
|
697
|
+
const req = createMockRequest();
|
|
698
|
+
const plainText = 'This is a plain text response';
|
|
699
|
+
const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(plainText)), 'text/plain');
|
|
700
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
701
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
702
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
703
|
+
expect(loggedData.responseBody).toBe(plainText);
|
|
704
|
+
});
|
|
705
|
+
it('should truncate long plain text successful responses', () => {
|
|
706
|
+
const req = createMockRequest();
|
|
707
|
+
const longText = 'a'.repeat(300);
|
|
708
|
+
const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(longText)), 'text/plain');
|
|
709
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
710
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
711
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
712
|
+
expect(loggedData.responseBody).toBe(`${'a'.repeat(256)}... (truncated)`);
|
|
713
|
+
});
|
|
714
|
+
it('should not truncate long plain text failed responses', () => {
|
|
715
|
+
const req = createMockRequest();
|
|
716
|
+
const longText = 'a'.repeat(150);
|
|
717
|
+
const response = createMockResponseWithBody(500, new Uint8Array(Buffer.from(longText)), 'text/plain');
|
|
718
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
719
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
720
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
721
|
+
expect(loggedData.responseBody).toBe(longText);
|
|
722
|
+
expect(loggedData.success).toBe(false);
|
|
600
723
|
});
|
|
601
724
|
});
|
|
602
725
|
describe('edge cases', () => {
|
|
@@ -637,7 +760,7 @@ describe('ToolLogger', () => {
|
|
|
637
760
|
string: 'text',
|
|
638
761
|
number: 42,
|
|
639
762
|
boolean: true,
|
|
640
|
-
array: [1, 2
|
|
763
|
+
array: [1, 2],
|
|
641
764
|
object: { nested: 'value' },
|
|
642
765
|
nullValue: null,
|
|
643
766
|
password: 'secret'
|
|
@@ -653,7 +776,7 @@ describe('ToolLogger', () => {
|
|
|
653
776
|
string: 'text',
|
|
654
777
|
number: 42,
|
|
655
778
|
boolean: true,
|
|
656
|
-
array: [1, 2
|
|
779
|
+
array: [1, 2],
|
|
657
780
|
object: { nested: 'value' },
|
|
658
781
|
nullValue: null,
|
|
659
782
|
password: '[REDACTED]'
|
|
@@ -729,24 +852,7 @@ describe('ToolLogger', () => {
|
|
|
729
852
|
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
730
853
|
required: true
|
|
731
854
|
},
|
|
732
|
-
|
|
733
|
-
name: 'sigLevel',
|
|
734
|
-
type: 'number',
|
|
735
|
-
description: 'OVERRIDDEN: Enhanced statistical significance level',
|
|
736
|
-
required: true
|
|
737
|
-
},
|
|
738
|
-
{
|
|
739
|
-
name: 'numVariations',
|
|
740
|
-
type: 'number',
|
|
741
|
-
description: 'OVERRIDDEN: Enhanced number of variations handling',
|
|
742
|
-
required: true
|
|
743
|
-
},
|
|
744
|
-
{
|
|
745
|
-
name: 'dailyVisitors',
|
|
746
|
-
type: 'number',
|
|
747
|
-
description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
|
|
748
|
-
required: true
|
|
749
|
-
}
|
|
855
|
+
'... (3 more items truncated)'
|
|
750
856
|
]
|
|
751
857
|
}
|
|
752
858
|
]
|