@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.1 → 1.0.0-beta.10
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 +169 -3
- package/dist/auth/AuthUtils.d.ts +12 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +80 -25
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/auth/AuthUtils.test.js +161 -117
- package/dist/auth/AuthUtils.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +5 -3
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +32 -8
- package/dist/function/GlobalToolFunction.js.map +1 -1
- package/dist/function/GlobalToolFunction.test.js +73 -12
- package/dist/function/GlobalToolFunction.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +11 -4
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +45 -9
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +278 -11
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/ToolLogger.d.ts +42 -0
- package/dist/logging/ToolLogger.d.ts.map +1 -0
- package/dist/logging/ToolLogger.js +255 -0
- package/dist/logging/ToolLogger.js.map +1 -0
- package/dist/logging/ToolLogger.test.d.ts +2 -0
- package/dist/logging/ToolLogger.test.d.ts.map +1 -0
- package/dist/logging/ToolLogger.test.js +864 -0
- package/dist/logging/ToolLogger.test.js.map +1 -0
- package/dist/service/Service.d.ts +88 -2
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +228 -39
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +558 -22
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +7 -1
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +5 -1
- package/dist/types/Models.js.map +1 -1
- package/dist/types/ToolError.d.ts +72 -0
- package/dist/types/ToolError.d.ts.map +1 -0
- package/dist/types/ToolError.js +107 -0
- package/dist/types/ToolError.js.map +1 -0
- package/dist/types/ToolError.test.d.ts +2 -0
- package/dist/types/ToolError.test.d.ts.map +1 -0
- package/dist/types/ToolError.test.js +185 -0
- package/dist/types/ToolError.test.js.map +1 -0
- package/dist/validation/ParameterValidator.d.ts +31 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +129 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +323 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -3
- package/src/auth/AuthUtils.test.ts +176 -157
- package/src/auth/AuthUtils.ts +96 -33
- package/src/function/GlobalToolFunction.test.ts +78 -14
- package/src/function/GlobalToolFunction.ts +46 -11
- package/src/function/ToolFunction.test.ts +298 -13
- package/src/function/ToolFunction.ts +61 -13
- package/src/index.ts +2 -1
- package/src/logging/ToolLogger.test.ts +1020 -0
- package/src/logging/ToolLogger.ts +292 -0
- package/src/service/Service.test.ts +712 -28
- package/src/service/Service.ts +288 -38
- package/src/types/Models.ts +8 -1
- package/src/types/ToolError.test.ts +222 -0
- package/src/types/ToolError.ts +125 -0
- package/src/validation/ParameterValidator.test.ts +371 -0
- package/src/validation/ParameterValidator.ts +150 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ToolLogger_1 = require("./ToolLogger");
|
|
4
|
+
const app_sdk_1 = require("@zaiusinc/app-sdk");
|
|
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
|
+
describe('ToolLogger', () => {
|
|
17
|
+
const mockLogger = app_sdk_1.logger;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
// Helper function to check JSON string logs
|
|
22
|
+
const expectJsonLog = (expectedData) => {
|
|
23
|
+
expect(mockLogger.info).toHaveBeenCalledWith(app_sdk_1.LogVisibility.Zaius, JSON.stringify(expectedData));
|
|
24
|
+
};
|
|
25
|
+
const createMockRequest = (overrides = {}) => {
|
|
26
|
+
const defaultRequest = {
|
|
27
|
+
path: '/test-tool',
|
|
28
|
+
method: 'POST',
|
|
29
|
+
bodyJSON: {
|
|
30
|
+
parameters: {
|
|
31
|
+
name: 'test',
|
|
32
|
+
value: 'data'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
headers: {
|
|
36
|
+
get: jest.fn().mockReturnValue('application/json')
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return { ...defaultRequest, ...overrides };
|
|
40
|
+
};
|
|
41
|
+
const createMockResponse = (status = 200, bodyJSON = {}, headers = {}) => {
|
|
42
|
+
const mockHeaders = {
|
|
43
|
+
get: jest.fn().mockImplementation((name) => {
|
|
44
|
+
if (name === 'content-type')
|
|
45
|
+
return 'application/json';
|
|
46
|
+
return headers[name] || null;
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
const response = {
|
|
50
|
+
status,
|
|
51
|
+
headers: mockHeaders,
|
|
52
|
+
_bodyJSON: bodyJSON
|
|
53
|
+
};
|
|
54
|
+
// Add getter for bodyJSON that returns the stored value
|
|
55
|
+
Object.defineProperty(response, 'bodyJSON', {
|
|
56
|
+
get() {
|
|
57
|
+
return this._bodyJSON;
|
|
58
|
+
},
|
|
59
|
+
set(value) {
|
|
60
|
+
this._bodyJSON = value;
|
|
61
|
+
},
|
|
62
|
+
enumerable: true
|
|
63
|
+
});
|
|
64
|
+
// Add getter for bodyAsU8Array that recalculates based on current bodyJSON
|
|
65
|
+
Object.defineProperty(response, 'bodyAsU8Array', {
|
|
66
|
+
get() {
|
|
67
|
+
if (this._bodyJSON !== null && this._bodyJSON !== undefined) {
|
|
68
|
+
// This will throw for circular references, which matches real behavior
|
|
69
|
+
const jsonString = JSON.stringify(this._bodyJSON);
|
|
70
|
+
return new Uint8Array(Buffer.from(jsonString));
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
enumerable: true
|
|
75
|
+
});
|
|
76
|
+
return response;
|
|
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
|
+
};
|
|
94
|
+
describe('logRequest', () => {
|
|
95
|
+
it('should log request with parameters', () => {
|
|
96
|
+
const req = createMockRequest();
|
|
97
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
98
|
+
const expectedLog = {
|
|
99
|
+
event: 'opal_tool_request',
|
|
100
|
+
path: '/test-tool',
|
|
101
|
+
method: 'POST',
|
|
102
|
+
parameters: {
|
|
103
|
+
name: 'test',
|
|
104
|
+
value: 'data'
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
expect(mockLogger.info).toHaveBeenCalledWith(app_sdk_1.LogVisibility.Zaius, JSON.stringify(expectedLog));
|
|
108
|
+
});
|
|
109
|
+
it('should handle request without parameters', () => {
|
|
110
|
+
const req = createMockRequest({
|
|
111
|
+
bodyJSON: null
|
|
112
|
+
});
|
|
113
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
114
|
+
expectJsonLog({
|
|
115
|
+
event: 'opal_tool_request',
|
|
116
|
+
path: '/test-tool',
|
|
117
|
+
method: 'POST',
|
|
118
|
+
parameters: null
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it('should use bodyJSON as parameters when no parameters field exists', () => {
|
|
122
|
+
const req = createMockRequest({
|
|
123
|
+
bodyJSON: {
|
|
124
|
+
name: 'direct',
|
|
125
|
+
action: 'test'
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
129
|
+
expectJsonLog({
|
|
130
|
+
event: 'opal_tool_request',
|
|
131
|
+
path: '/test-tool',
|
|
132
|
+
method: 'POST',
|
|
133
|
+
parameters: {
|
|
134
|
+
name: 'direct',
|
|
135
|
+
action: 'test'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('should redact all sensitive field variations', () => {
|
|
140
|
+
const req = createMockRequest({
|
|
141
|
+
bodyJSON: {
|
|
142
|
+
parameters: {
|
|
143
|
+
username: 'john',
|
|
144
|
+
password: 'secret123',
|
|
145
|
+
api_key: 'key123',
|
|
146
|
+
secret: 'mysecret',
|
|
147
|
+
token: 'abc123',
|
|
148
|
+
auth: 'authdata',
|
|
149
|
+
credentials: 'creds',
|
|
150
|
+
access_token: 'access123',
|
|
151
|
+
refresh_token: 'refresh123',
|
|
152
|
+
private_key: 'privatekey',
|
|
153
|
+
client_secret: 'clientsecret',
|
|
154
|
+
normal_field: 'visible'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
159
|
+
expectJsonLog({
|
|
160
|
+
event: 'opal_tool_request',
|
|
161
|
+
path: '/test-tool',
|
|
162
|
+
method: 'POST',
|
|
163
|
+
parameters: {
|
|
164
|
+
username: 'john',
|
|
165
|
+
password: '[REDACTED]',
|
|
166
|
+
api_key: '[REDACTED]',
|
|
167
|
+
secret: '[REDACTED]',
|
|
168
|
+
token: '[REDACTED]',
|
|
169
|
+
auth: '[REDACTED]',
|
|
170
|
+
credentials: '[REDACTED]',
|
|
171
|
+
access_token: '[REDACTED]',
|
|
172
|
+
refresh_token: '[REDACTED]',
|
|
173
|
+
private_key: '[REDACTED]',
|
|
174
|
+
client_secret: '[REDACTED]',
|
|
175
|
+
normal_field: 'visible'
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
it('should redact sensitive fields with case variations', () => {
|
|
180
|
+
const req = createMockRequest({
|
|
181
|
+
bodyJSON: {
|
|
182
|
+
parameters: {
|
|
183
|
+
PASSWORD: 'secret1',
|
|
184
|
+
API_KEY: 'secret2',
|
|
185
|
+
clientSecret: 'secret3',
|
|
186
|
+
user_password: 'secret4',
|
|
187
|
+
oauth_token: 'secret5',
|
|
188
|
+
ssh_key: 'secret6',
|
|
189
|
+
normal_field: 'visible'
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
194
|
+
expectJsonLog({
|
|
195
|
+
event: 'opal_tool_request',
|
|
196
|
+
path: '/test-tool',
|
|
197
|
+
method: 'POST',
|
|
198
|
+
parameters: {
|
|
199
|
+
PASSWORD: '[REDACTED]',
|
|
200
|
+
API_KEY: '[REDACTED]',
|
|
201
|
+
clientSecret: '[REDACTED]',
|
|
202
|
+
user_password: '[REDACTED]',
|
|
203
|
+
oauth_token: '[REDACTED]',
|
|
204
|
+
ssh_key: '[REDACTED]',
|
|
205
|
+
normal_field: 'visible'
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
it('should truncate long string values', () => {
|
|
210
|
+
const longString = 'a'.repeat(150);
|
|
211
|
+
const req = createMockRequest({
|
|
212
|
+
bodyJSON: {
|
|
213
|
+
parameters: {
|
|
214
|
+
description: longString,
|
|
215
|
+
short_field: 'normal'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
220
|
+
expectJsonLog({
|
|
221
|
+
event: 'opal_tool_request',
|
|
222
|
+
path: '/test-tool',
|
|
223
|
+
method: 'POST',
|
|
224
|
+
parameters: {
|
|
225
|
+
description: `${'a'.repeat(118)}...[22 truncated]...${'a'.repeat(10)}`,
|
|
226
|
+
short_field: 'normal'
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
it('should truncate large arrays', () => {
|
|
231
|
+
const largeArray = Array.from({ length: 15 }, (_, i) => `item${i}`);
|
|
232
|
+
const req = createMockRequest({
|
|
233
|
+
bodyJSON: {
|
|
234
|
+
parameters: {
|
|
235
|
+
items: largeArray,
|
|
236
|
+
small_array: ['a', 'b']
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
241
|
+
expectJsonLog({
|
|
242
|
+
event: 'opal_tool_request',
|
|
243
|
+
path: '/test-tool',
|
|
244
|
+
method: 'POST',
|
|
245
|
+
parameters: {
|
|
246
|
+
items: [
|
|
247
|
+
...largeArray.slice(0, 2),
|
|
248
|
+
'... (13 more items truncated)'
|
|
249
|
+
],
|
|
250
|
+
small_array: ['a', 'b']
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should handle nested objects with sensitive fields', () => {
|
|
255
|
+
const req = createMockRequest({
|
|
256
|
+
bodyJSON: {
|
|
257
|
+
parameters: {
|
|
258
|
+
user: {
|
|
259
|
+
name: 'John',
|
|
260
|
+
email: 'john@example.com',
|
|
261
|
+
password: 'secret123'
|
|
262
|
+
},
|
|
263
|
+
config: {
|
|
264
|
+
database: {
|
|
265
|
+
host: 'localhost',
|
|
266
|
+
port: 5432,
|
|
267
|
+
password: 'dbpass'
|
|
268
|
+
},
|
|
269
|
+
api_key: 'apikey123'
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
275
|
+
expectJsonLog({
|
|
276
|
+
event: 'opal_tool_request',
|
|
277
|
+
path: '/test-tool',
|
|
278
|
+
method: 'POST',
|
|
279
|
+
parameters: {
|
|
280
|
+
user: {
|
|
281
|
+
name: 'John',
|
|
282
|
+
email: '[REDACTED]',
|
|
283
|
+
password: '[REDACTED]'
|
|
284
|
+
},
|
|
285
|
+
config: {
|
|
286
|
+
database: {
|
|
287
|
+
host: 'localhost',
|
|
288
|
+
port: 5432,
|
|
289
|
+
password: '[REDACTED]'
|
|
290
|
+
},
|
|
291
|
+
api_key: '[REDACTED]'
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
it('should handle null and undefined values', () => {
|
|
297
|
+
const req = createMockRequest({
|
|
298
|
+
bodyJSON: {
|
|
299
|
+
parameters: {
|
|
300
|
+
nullValue: null,
|
|
301
|
+
undefinedValue: undefined,
|
|
302
|
+
emptyString: '',
|
|
303
|
+
zero: 0,
|
|
304
|
+
false: false,
|
|
305
|
+
password: null // sensitive field with null value
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
310
|
+
expectJsonLog({
|
|
311
|
+
event: 'opal_tool_request',
|
|
312
|
+
path: '/test-tool',
|
|
313
|
+
method: 'POST',
|
|
314
|
+
parameters: {
|
|
315
|
+
nullValue: null,
|
|
316
|
+
emptyString: '',
|
|
317
|
+
zero: 0,
|
|
318
|
+
false: false,
|
|
319
|
+
password: '[REDACTED]'
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
it('should handle arrays in sensitive fields', () => {
|
|
324
|
+
const req = createMockRequest({
|
|
325
|
+
bodyJSON: {
|
|
326
|
+
parameters: {
|
|
327
|
+
credentials: ['user', 'pass', 'token'],
|
|
328
|
+
public_list: ['item1', 'item2']
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
333
|
+
expectJsonLog({
|
|
334
|
+
event: 'opal_tool_request',
|
|
335
|
+
path: '/test-tool',
|
|
336
|
+
method: 'POST',
|
|
337
|
+
parameters: {
|
|
338
|
+
credentials: '[REDACTED]',
|
|
339
|
+
public_list: ['item1', 'item2']
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
it('should handle objects in sensitive fields', () => {
|
|
344
|
+
const req = createMockRequest({
|
|
345
|
+
bodyJSON: {
|
|
346
|
+
parameters: {
|
|
347
|
+
auth: {
|
|
348
|
+
username: 'john',
|
|
349
|
+
password: 'secret'
|
|
350
|
+
},
|
|
351
|
+
public_config: {
|
|
352
|
+
timeout: 30,
|
|
353
|
+
retries: 3
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
359
|
+
expectJsonLog({
|
|
360
|
+
event: 'opal_tool_request',
|
|
361
|
+
path: '/test-tool',
|
|
362
|
+
method: 'POST',
|
|
363
|
+
parameters: {
|
|
364
|
+
auth: '[REDACTED]',
|
|
365
|
+
public_config: {
|
|
366
|
+
timeout: 30,
|
|
367
|
+
retries: 3
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
it('should respect max depth to prevent infinite recursion', () => {
|
|
373
|
+
const deepObject = { level: 0, data: 'test' };
|
|
374
|
+
let current = deepObject;
|
|
375
|
+
// Create a very deep nested object (deeper than maxDepth)
|
|
376
|
+
for (let i = 1; i <= 10; i++) {
|
|
377
|
+
current.nested = { level: i, data: `level${i}` };
|
|
378
|
+
current = current.nested;
|
|
379
|
+
}
|
|
380
|
+
const req = createMockRequest({
|
|
381
|
+
bodyJSON: { parameters: { deep: deepObject } }
|
|
382
|
+
});
|
|
383
|
+
// Should not throw error or cause infinite recursion
|
|
384
|
+
expect(() => ToolLogger_1.ToolLogger.logRequest(req)).not.toThrow();
|
|
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
|
|
399
|
+
});
|
|
400
|
+
it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
|
|
401
|
+
// Create an object with exactly 6 levels (exceeds maxDepth of 5)
|
|
402
|
+
const deepObject = {
|
|
403
|
+
level1: {
|
|
404
|
+
level2: {
|
|
405
|
+
level3: {
|
|
406
|
+
level4: {
|
|
407
|
+
level5: {
|
|
408
|
+
level6: {
|
|
409
|
+
password: 'should-not-be-visible',
|
|
410
|
+
credit_card: '1234-5678-9012-3456',
|
|
411
|
+
data: 'sensitive-info'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const req = createMockRequest({
|
|
420
|
+
bodyJSON: { parameters: { deep: deepObject } }
|
|
421
|
+
});
|
|
422
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
423
|
+
// Verify that the deeply nested object is replaced with placeholder
|
|
424
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
425
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
426
|
+
// Navigate to the deeply nested part that should be replaced
|
|
427
|
+
// At maxDepth=5, the 5th level (level4) gets replaced with the placeholder
|
|
428
|
+
const level4 = loggedData.parameters.deep.level1.level2.level3.level4;
|
|
429
|
+
expect(level4).toBe('[MAX_DEPTH_EXCEEDED]');
|
|
430
|
+
});
|
|
431
|
+
it('should handle arrays containing deeply nested objects that exceed max depth', () => {
|
|
432
|
+
// Create a structure where the array is shallow enough to be processed (depth 3),
|
|
433
|
+
// but individual objects within the array exceed the max depth
|
|
434
|
+
const arrayWithDeepObjects = {
|
|
435
|
+
level1: {
|
|
436
|
+
items: [
|
|
437
|
+
{
|
|
438
|
+
shallow: 'data',
|
|
439
|
+
level2: {
|
|
440
|
+
level3: {
|
|
441
|
+
level4: {
|
|
442
|
+
level5: {
|
|
443
|
+
password: 'secret-in-deep-array-object',
|
|
444
|
+
credit_card: '1234-5678-9012-3456'
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
'simple-item',
|
|
451
|
+
{
|
|
452
|
+
shallow: 'data'
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
level2: {
|
|
456
|
+
level3: {
|
|
457
|
+
level4: {
|
|
458
|
+
another: 'deep-object'
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const req = createMockRequest({
|
|
467
|
+
bodyJSON: { parameters: arrayWithDeepObjects }
|
|
468
|
+
});
|
|
469
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
470
|
+
// Verify the array structure and depth handling
|
|
471
|
+
const logCall = mockLogger.info.mock.calls[0];
|
|
472
|
+
const loggedData = JSON.parse(logCall[1]);
|
|
473
|
+
const items = loggedData.parameters.level1.items;
|
|
474
|
+
expect(items.length).toBe(3);
|
|
475
|
+
// First item: deeply nested object with inner parts replaced by placeholder
|
|
476
|
+
// shallow object should be processed normally
|
|
477
|
+
expect(items[0]).toEqual({
|
|
478
|
+
shallow: 'data',
|
|
479
|
+
level2: {
|
|
480
|
+
level3: {
|
|
481
|
+
level4: '[MAX_DEPTH_EXCEEDED]'
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// Second item: simple string should remain unchanged
|
|
486
|
+
expect(items[1]).toBe('simple-item');
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
describe('logResponse', () => {
|
|
490
|
+
it('should log successful response with all details', () => {
|
|
491
|
+
const req = createMockRequest();
|
|
492
|
+
const response = createMockResponse(200, { result: 'success', data: 'test' });
|
|
493
|
+
ToolLogger_1.ToolLogger.logResponse(req, response, 150);
|
|
494
|
+
const expectedLog = {
|
|
495
|
+
event: 'opal_tool_response',
|
|
496
|
+
path: '/test-tool',
|
|
497
|
+
duration: '150ms',
|
|
498
|
+
status: 200,
|
|
499
|
+
contentType: 'application/json',
|
|
500
|
+
contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
|
|
501
|
+
success: true,
|
|
502
|
+
responseBody: { result: 'success', data: 'test' }
|
|
503
|
+
};
|
|
504
|
+
expect(mockLogger.info).toHaveBeenCalledWith(app_sdk_1.LogVisibility.Zaius, JSON.stringify(expectedLog));
|
|
505
|
+
});
|
|
506
|
+
it('should log error response', () => {
|
|
507
|
+
const req = createMockRequest();
|
|
508
|
+
const response = createMockResponse(400, { error: 'Bad request' });
|
|
509
|
+
ToolLogger_1.ToolLogger.logResponse(req, response, 75);
|
|
510
|
+
expectJsonLog({
|
|
511
|
+
event: 'opal_tool_response',
|
|
512
|
+
path: '/test-tool',
|
|
513
|
+
duration: '75ms',
|
|
514
|
+
status: 400,
|
|
515
|
+
contentType: 'application/json',
|
|
516
|
+
contentLength: 23,
|
|
517
|
+
success: false,
|
|
518
|
+
responseBody: { error: 'Bad request' }
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
it('should handle response without body data', () => {
|
|
522
|
+
const req = createMockRequest();
|
|
523
|
+
const response = createMockResponseWithBody(204, undefined, 'application/json');
|
|
524
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
525
|
+
expectJsonLog({
|
|
526
|
+
event: 'opal_tool_response',
|
|
527
|
+
path: '/test-tool',
|
|
528
|
+
status: 204,
|
|
529
|
+
contentType: 'application/json',
|
|
530
|
+
contentLength: 'unknown',
|
|
531
|
+
success: true
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
it('should handle response without processing time', () => {
|
|
535
|
+
const req = createMockRequest();
|
|
536
|
+
const response = createMockResponse(200, { data: 'test' });
|
|
537
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
538
|
+
expectJsonLog({
|
|
539
|
+
event: 'opal_tool_response',
|
|
540
|
+
path: '/test-tool',
|
|
541
|
+
status: 200,
|
|
542
|
+
contentType: 'application/json',
|
|
543
|
+
contentLength: 15,
|
|
544
|
+
success: true,
|
|
545
|
+
responseBody: { data: 'test' }
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
it('should handle unknown content type - response body not logged', () => {
|
|
549
|
+
const req = createMockRequest();
|
|
550
|
+
const response = createMockResponse(200, { data: 'test' });
|
|
551
|
+
response.headers.get = jest.fn().mockReturnValue(null);
|
|
552
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
553
|
+
expectJsonLog({
|
|
554
|
+
event: 'opal_tool_response',
|
|
555
|
+
path: '/test-tool',
|
|
556
|
+
status: 200,
|
|
557
|
+
contentType: 'unknown',
|
|
558
|
+
contentLength: 15,
|
|
559
|
+
success: true
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
it('should handle content length calculation error', () => {
|
|
563
|
+
const req = createMockRequest();
|
|
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 = {
|
|
570
|
+
status: 200,
|
|
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
|
|
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();
|
|
587
|
+
});
|
|
588
|
+
it('should correctly identify success status codes', () => {
|
|
589
|
+
const req = createMockRequest();
|
|
590
|
+
const testCases = [
|
|
591
|
+
{ status: 200, expected: true },
|
|
592
|
+
{ status: 201, expected: true },
|
|
593
|
+
{ status: 204, expected: true },
|
|
594
|
+
{ status: 299, expected: true },
|
|
595
|
+
{ status: 300, expected: false },
|
|
596
|
+
{ status: 400, expected: false },
|
|
597
|
+
{ status: 404, expected: false },
|
|
598
|
+
{ status: 500, expected: false }
|
|
599
|
+
];
|
|
600
|
+
testCases.forEach(({ status, expected }) => {
|
|
601
|
+
mockLogger.info.mockClear();
|
|
602
|
+
const response = createMockResponse(status);
|
|
603
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
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({});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
it('should handle different content types', () => {
|
|
616
|
+
const req = createMockRequest();
|
|
617
|
+
const testCases = [
|
|
618
|
+
{ contentType: 'application/json', expectedBody: { data: 'test' } },
|
|
619
|
+
{ contentType: 'text/plain', expectedBody: '{"data":"test"}' },
|
|
620
|
+
{ contentType: 'text/html', expectedBody: '{"data":"test"}' }
|
|
621
|
+
];
|
|
622
|
+
testCases.forEach(({ contentType, expectedBody }) => {
|
|
623
|
+
mockLogger.info.mockClear();
|
|
624
|
+
const response = createMockResponse(200, { data: 'test' });
|
|
625
|
+
response.headers.get = jest.fn().mockReturnValue(contentType);
|
|
626
|
+
ToolLogger_1.ToolLogger.logResponse(req, response);
|
|
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'
|
|
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);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
describe('edge cases', () => {
|
|
726
|
+
it('should handle empty request bodyJSON', () => {
|
|
727
|
+
const req = createMockRequest({
|
|
728
|
+
bodyJSON: {}
|
|
729
|
+
});
|
|
730
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
731
|
+
expectJsonLog({
|
|
732
|
+
event: 'opal_tool_request',
|
|
733
|
+
path: '/test-tool',
|
|
734
|
+
method: 'POST',
|
|
735
|
+
parameters: {}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
it('should handle request with only parameters field', () => {
|
|
739
|
+
const req = createMockRequest({
|
|
740
|
+
bodyJSON: {
|
|
741
|
+
parameters: {
|
|
742
|
+
field: 'value' // Changed from 'key' to 'field' to avoid sensitive field detection
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
747
|
+
expectJsonLog({
|
|
748
|
+
event: 'opal_tool_request',
|
|
749
|
+
path: '/test-tool',
|
|
750
|
+
method: 'POST',
|
|
751
|
+
parameters: {
|
|
752
|
+
field: 'value'
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
it('should handle mixed data types in parameters', () => {
|
|
757
|
+
const req = createMockRequest({
|
|
758
|
+
bodyJSON: {
|
|
759
|
+
parameters: {
|
|
760
|
+
string: 'text',
|
|
761
|
+
number: 42,
|
|
762
|
+
boolean: true,
|
|
763
|
+
array: [1, 2],
|
|
764
|
+
object: { nested: 'value' },
|
|
765
|
+
nullValue: null,
|
|
766
|
+
password: 'secret'
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
771
|
+
expectJsonLog({
|
|
772
|
+
event: 'opal_tool_request',
|
|
773
|
+
path: '/test-tool',
|
|
774
|
+
method: 'POST',
|
|
775
|
+
parameters: {
|
|
776
|
+
string: 'text',
|
|
777
|
+
number: 42,
|
|
778
|
+
boolean: true,
|
|
779
|
+
array: [1, 2],
|
|
780
|
+
object: { nested: 'value' },
|
|
781
|
+
nullValue: null,
|
|
782
|
+
password: '[REDACTED]'
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
it('should handle tool override request with enhanced parameter descriptions', () => {
|
|
787
|
+
const overrideRequest = {
|
|
788
|
+
tools: [
|
|
789
|
+
{
|
|
790
|
+
name: 'calculate_experiment_runtime',
|
|
791
|
+
description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
|
|
792
|
+
parameters: [
|
|
793
|
+
{
|
|
794
|
+
name: 'BCR',
|
|
795
|
+
type: 'number',
|
|
796
|
+
description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
|
|
797
|
+
required: true
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: 'MDE',
|
|
801
|
+
type: 'number',
|
|
802
|
+
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
803
|
+
required: true
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
name: 'sigLevel',
|
|
807
|
+
type: 'number',
|
|
808
|
+
description: 'OVERRIDDEN: Enhanced statistical significance level',
|
|
809
|
+
required: true
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: 'numVariations',
|
|
813
|
+
type: 'number',
|
|
814
|
+
description: 'OVERRIDDEN: Enhanced number of variations handling',
|
|
815
|
+
required: true
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
name: 'dailyVisitors',
|
|
819
|
+
type: 'number',
|
|
820
|
+
description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
|
|
821
|
+
required: true
|
|
822
|
+
}
|
|
823
|
+
]
|
|
824
|
+
}
|
|
825
|
+
// Note: NOT including calculate_sample_size in override
|
|
826
|
+
]
|
|
827
|
+
};
|
|
828
|
+
const req = createMockRequest({
|
|
829
|
+
path: '/overrides',
|
|
830
|
+
bodyJSON: overrideRequest
|
|
831
|
+
});
|
|
832
|
+
ToolLogger_1.ToolLogger.logRequest(req);
|
|
833
|
+
expectJsonLog({
|
|
834
|
+
event: 'opal_tool_request',
|
|
835
|
+
path: '/overrides',
|
|
836
|
+
method: 'POST',
|
|
837
|
+
parameters: {
|
|
838
|
+
tools: [
|
|
839
|
+
{
|
|
840
|
+
name: 'calculate_experiment_runtime',
|
|
841
|
+
description: 'OVERRIDDEN: Enhanced experiment runtime calculator with advanced features',
|
|
842
|
+
parameters: [
|
|
843
|
+
{
|
|
844
|
+
name: 'BCR',
|
|
845
|
+
type: 'number',
|
|
846
|
+
description: 'OVERRIDDEN: Enhanced baseline conversion rate with validation',
|
|
847
|
+
required: true
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
name: 'MDE',
|
|
851
|
+
type: 'number',
|
|
852
|
+
description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
|
|
853
|
+
required: true
|
|
854
|
+
},
|
|
855
|
+
'... (3 more items truncated)'
|
|
856
|
+
]
|
|
857
|
+
}
|
|
858
|
+
]
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
//# sourceMappingURL=ToolLogger.test.js.map
|