@optimizely-opal/opal-tool-ocp-sdk 1.0.0 → 1.1.0-beta.2
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 +51 -13
- package/dist/auth/AuthUtils.d.ts +2 -5
- package/dist/auth/AuthUtils.d.ts.map +1 -1
- package/dist/auth/AuthUtils.js +5 -5
- package/dist/auth/AuthUtils.js.map +1 -1
- package/dist/decorator/Decorator.test.js +4 -4
- package/dist/decorator/Decorator.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +4 -1
- package/dist/function/GlobalToolFunction.d.ts.map +1 -1
- package/dist/function/GlobalToolFunction.js +27 -21
- 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/service/Service.d.ts +20 -19
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +47 -72
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +229 -133
- package/dist/service/Service.test.js.map +1 -1
- package/dist/types/Models.d.ts +18 -7
- package/dist/types/Models.d.ts.map +1 -1
- package/dist/types/Models.js +1 -29
- package/dist/types/Models.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 +1 -1
- package/src/auth/AuthUtils.ts +10 -10
- package/src/decorator/Decorator.test.ts +4 -4
- package/src/function/GlobalToolFunction.test.ts +113 -213
- package/src/function/GlobalToolFunction.ts +31 -31
- package/src/function/ToolFunction.test.ts +78 -285
- package/src/function/ToolFunction.ts +24 -30
- package/src/service/Service.test.ts +238 -174
- package/src/service/Service.ts +68 -92
- package/src/types/Models.ts +24 -15
- package/src/utils/ErrorFormatter.ts +31 -0
|
@@ -72,6 +72,10 @@ jest.mock('@zaiusinc/app-sdk', () => {
|
|
|
72
72
|
}))
|
|
73
73
|
};
|
|
74
74
|
});
|
|
75
|
+
// Mock the authenticateInternalRequest function
|
|
76
|
+
jest.mock('../auth/AuthUtils', () => ({
|
|
77
|
+
authenticateInternalRequest: jest.fn().mockResolvedValue(undefined)
|
|
78
|
+
}));
|
|
75
79
|
// Get the mocked kvStore for use in tests
|
|
76
80
|
const { storage } = jest.requireMock('@zaiusinc/app-sdk');
|
|
77
81
|
const mockKvStore = storage.kvStore;
|
|
@@ -110,11 +114,20 @@ describe('ToolsService', () => {
|
|
|
110
114
|
});
|
|
111
115
|
return map;
|
|
112
116
|
};
|
|
117
|
+
const defaultAuth = {
|
|
118
|
+
provider: 'OptiID',
|
|
119
|
+
credentials: {
|
|
120
|
+
customer_id: 'test-customer',
|
|
121
|
+
instance_id: 'test-instance',
|
|
122
|
+
access_token: 'test-token',
|
|
123
|
+
product_sku: 'test-sku'
|
|
124
|
+
}
|
|
125
|
+
};
|
|
113
126
|
const baseRequest = {
|
|
114
127
|
path: '/test-tool',
|
|
115
128
|
method: 'POST',
|
|
116
|
-
bodyJSON: { parameters: { param1: 'test-value' } },
|
|
117
|
-
body: JSON.stringify({ parameters: { param1: 'test-value' } }),
|
|
129
|
+
bodyJSON: { parameters: { param1: 'test-value' }, auth: defaultAuth },
|
|
130
|
+
body: JSON.stringify({ parameters: { param1: 'test-value' }, auth: defaultAuth }),
|
|
118
131
|
bodyData: Buffer.from(''),
|
|
119
132
|
headers: createHeadersMap(),
|
|
120
133
|
params: {},
|
|
@@ -194,8 +207,7 @@ describe('ToolsService', () => {
|
|
|
194
207
|
endpoint: '/second-tool',
|
|
195
208
|
http_method: 'POST',
|
|
196
209
|
auth_requirements: [
|
|
197
|
-
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
198
|
-
{ provider: 'OptiID', scope_bundle: 'default', required: true }
|
|
210
|
+
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
199
211
|
]
|
|
200
212
|
});
|
|
201
213
|
});
|
|
@@ -209,7 +221,7 @@ describe('ToolsService', () => {
|
|
|
209
221
|
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
|
|
210
222
|
expect(response.status).toBe(200);
|
|
211
223
|
expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, // functionContext
|
|
212
|
-
{ param1: 'test-value' },
|
|
224
|
+
{ param1: 'test-value' }, expect.objectContaining({ provider: 'OptiID' }));
|
|
213
225
|
});
|
|
214
226
|
it('should execute tool with existing ToolFunction instance context', async () => {
|
|
215
227
|
// Create a mock ToolFunction instance
|
|
@@ -225,7 +237,7 @@ describe('ToolsService', () => {
|
|
|
225
237
|
const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunctionInstance);
|
|
226
238
|
expect(response.status).toBe(200);
|
|
227
239
|
expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunctionInstance, // functionContext - existing instance
|
|
228
|
-
{ param1: 'test-value' },
|
|
240
|
+
{ param1: 'test-value' }, expect.objectContaining({ provider: 'OptiID' }));
|
|
229
241
|
});
|
|
230
242
|
it('should allow handler in ToolFunction subclass to access request object', async () => {
|
|
231
243
|
// Create a mock class that extends ToolFunction
|
|
@@ -255,9 +267,18 @@ describe('ToolsService', () => {
|
|
|
255
267
|
});
|
|
256
268
|
// Register a tool with our custom handler
|
|
257
269
|
Service_1.toolsService.registerTool('test-toolfunction-access', 'Test handler access to ToolFunction instance', handlerThatAccessesRequest, [], '/test-toolfunction-access');
|
|
270
|
+
const authData = {
|
|
271
|
+
provider: 'OptiID',
|
|
272
|
+
credentials: {
|
|
273
|
+
customer_id: 'test-customer',
|
|
274
|
+
instance_id: 'test-instance',
|
|
275
|
+
access_token: 'test-token',
|
|
276
|
+
product_sku: 'test-sku'
|
|
277
|
+
}
|
|
278
|
+
};
|
|
258
279
|
const testRequest = createMockRequest({
|
|
259
280
|
path: '/test-toolfunction-access',
|
|
260
|
-
bodyJSON: { action: 'test' }
|
|
281
|
+
bodyJSON: { action: 'test', auth: authData }
|
|
261
282
|
});
|
|
262
283
|
const response = await Service_1.toolsService.processRequest(testRequest, mockToolFunctionInstance);
|
|
263
284
|
expect(response.status).toBe(200);
|
|
@@ -265,12 +286,20 @@ describe('ToolsService', () => {
|
|
|
265
286
|
expect(response.data.success).toBe(true);
|
|
266
287
|
expect(response.data.requestPath).toBe('/test-path');
|
|
267
288
|
expect(response.data.testMethodResult).toBe('path: /test-path');
|
|
268
|
-
expect(response.data.receivedParams).toEqual({ action: 'test' });
|
|
289
|
+
expect(response.data.receivedParams).toEqual({ action: 'test', auth: authData });
|
|
269
290
|
expect(handlerThatAccessesRequest).toHaveBeenCalledWith(mockToolFunctionInstance, // functionContext is the ToolFunction instance
|
|
270
|
-
{ action: 'test' },
|
|
291
|
+
{ action: 'test', auth: authData }, authData);
|
|
271
292
|
});
|
|
272
293
|
it('should execute tool with OptiID auth data when provided', async () => {
|
|
273
|
-
const authData =
|
|
294
|
+
const authData = {
|
|
295
|
+
provider: 'OptiID',
|
|
296
|
+
credentials: {
|
|
297
|
+
customer_id: 'customer123',
|
|
298
|
+
instance_id: 'instance123',
|
|
299
|
+
access_token: 'token123',
|
|
300
|
+
product_sku: 'sku123'
|
|
301
|
+
}
|
|
302
|
+
};
|
|
274
303
|
const requestWithAuth = createMockRequest({
|
|
275
304
|
bodyJSON: {
|
|
276
305
|
parameters: { param1: 'test-value' },
|
|
@@ -287,84 +316,59 @@ describe('ToolsService', () => {
|
|
|
287
316
|
{ param1: 'test-value' }, authData);
|
|
288
317
|
});
|
|
289
318
|
it('should handle request body without parameters wrapper', async () => {
|
|
319
|
+
const authData = {
|
|
320
|
+
provider: 'OptiID',
|
|
321
|
+
credentials: {
|
|
322
|
+
customer_id: 'test-customer',
|
|
323
|
+
instance_id: 'test-instance',
|
|
324
|
+
access_token: 'test-token',
|
|
325
|
+
product_sku: 'test-sku'
|
|
326
|
+
}
|
|
327
|
+
};
|
|
290
328
|
const requestWithoutWrapper = createMockRequest({
|
|
291
|
-
bodyJSON: { param1: 'test-value' },
|
|
292
|
-
body: JSON.stringify({ param1: 'test-value' })
|
|
329
|
+
bodyJSON: { param1: 'test-value', auth: authData },
|
|
330
|
+
body: JSON.stringify({ param1: 'test-value', auth: authData })
|
|
293
331
|
});
|
|
294
332
|
const response = await Service_1.toolsService.processRequest(requestWithoutWrapper, mockToolFunction);
|
|
295
333
|
expect(response.status).toBe(200);
|
|
296
334
|
expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, // functionContext
|
|
297
|
-
{ param1: 'test-value' },
|
|
335
|
+
{ param1: 'test-value', auth: authData }, authData);
|
|
298
336
|
});
|
|
299
|
-
it('should
|
|
337
|
+
it('should throw error when tool handler throws a regular error', async () => {
|
|
300
338
|
const errorMessage = 'Tool execution failed';
|
|
301
339
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
302
340
|
const mockRequest = createMockRequest();
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
expect(response.bodyJSON).toEqual({
|
|
306
|
-
title: 'Internal Server Error',
|
|
307
|
-
status: 500,
|
|
308
|
-
detail: errorMessage,
|
|
309
|
-
instance: mockTool.endpoint
|
|
310
|
-
});
|
|
311
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
312
|
-
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockTool.name}:`, expect.any(Error));
|
|
341
|
+
await expect(Service_1.toolsService.processRequest(mockRequest, mockToolFunction))
|
|
342
|
+
.rejects.toThrow(errorMessage);
|
|
313
343
|
});
|
|
314
|
-
it('should
|
|
344
|
+
it('should throw when tool handler throws object without message', async () => {
|
|
315
345
|
jest.mocked(mockTool.handler).mockRejectedValueOnce({});
|
|
316
346
|
const mockRequest = createMockRequest();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
expect(response.bodyJSON).toEqual({
|
|
320
|
-
title: 'Internal Server Error',
|
|
321
|
-
status: 500,
|
|
322
|
-
detail: 'An unexpected error occurred',
|
|
323
|
-
instance: mockTool.endpoint
|
|
324
|
-
});
|
|
325
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
347
|
+
await expect(Service_1.toolsService.processRequest(mockRequest, mockToolFunction))
|
|
348
|
+
.rejects.toEqual({});
|
|
326
349
|
});
|
|
327
|
-
it('should
|
|
350
|
+
it('should throw ToolError when tool handler throws ToolError', async () => {
|
|
328
351
|
const toolError = new ToolError_1.ToolError('Resource not found', 404, 'The requested task does not exist');
|
|
329
352
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
330
353
|
const mockRequest = createMockRequest();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
expect(response.bodyJSON).toEqual({
|
|
334
|
-
title: 'Resource not found',
|
|
335
|
-
status: 404,
|
|
336
|
-
detail: 'The requested task does not exist',
|
|
337
|
-
instance: mockTool.endpoint
|
|
338
|
-
});
|
|
339
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
340
|
-
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockTool.name}:`, expect.any(ToolError_1.ToolError));
|
|
354
|
+
await expect(Service_1.toolsService.processRequest(mockRequest, mockToolFunction))
|
|
355
|
+
.rejects.toThrow(toolError);
|
|
341
356
|
});
|
|
342
|
-
it('should
|
|
357
|
+
it('should throw ToolError without detail when detail is not provided', async () => {
|
|
343
358
|
const toolError = new ToolError_1.ToolError('Bad request', 400);
|
|
344
359
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
345
360
|
const mockRequest = createMockRequest();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
expect(
|
|
349
|
-
title: 'Bad request',
|
|
350
|
-
status: 400,
|
|
351
|
-
instance: mockTool.endpoint
|
|
352
|
-
});
|
|
353
|
-
expect(response.bodyJSON).not.toHaveProperty('detail');
|
|
354
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
361
|
+
await expect(Service_1.toolsService.processRequest(mockRequest, mockToolFunction))
|
|
362
|
+
.rejects.toThrow(toolError);
|
|
363
|
+
expect(toolError.status).toBe(400);
|
|
355
364
|
});
|
|
356
|
-
it('should default
|
|
365
|
+
it('should throw ToolError with default 500 status when created without status', async () => {
|
|
357
366
|
const toolError = new ToolError_1.ToolError('Database error');
|
|
358
367
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
359
368
|
const mockRequest = createMockRequest();
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
expect(
|
|
363
|
-
title: 'Database error',
|
|
364
|
-
status: 500,
|
|
365
|
-
instance: mockTool.endpoint
|
|
366
|
-
});
|
|
367
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
369
|
+
await expect(Service_1.toolsService.processRequest(mockRequest, mockToolFunction))
|
|
370
|
+
.rejects.toThrow(toolError);
|
|
371
|
+
expect(toolError.status).toBe(500);
|
|
368
372
|
});
|
|
369
373
|
});
|
|
370
374
|
describe('interaction execution', () => {
|
|
@@ -372,27 +376,54 @@ describe('ToolsService', () => {
|
|
|
372
376
|
Service_1.toolsService.registerInteraction(mockInteraction.name, mockInteraction.handler, mockInteraction.endpoint);
|
|
373
377
|
});
|
|
374
378
|
it('should execute interaction successfully with data', async () => {
|
|
379
|
+
const authData = {
|
|
380
|
+
provider: 'OptiID',
|
|
381
|
+
credentials: {
|
|
382
|
+
customer_id: 'test-customer',
|
|
383
|
+
instance_id: 'test-instance',
|
|
384
|
+
access_token: 'test-token',
|
|
385
|
+
product_sku: 'test-sku'
|
|
386
|
+
}
|
|
387
|
+
};
|
|
375
388
|
const interactionRequest = createMockRequest({
|
|
376
389
|
path: '/test-interaction',
|
|
377
|
-
bodyJSON: { data: { param1: 'test-value' } },
|
|
378
|
-
body: JSON.stringify({ data: { param1: 'test-value' } })
|
|
390
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData },
|
|
391
|
+
body: JSON.stringify({ data: { param1: 'test-value' }, auth: authData })
|
|
379
392
|
});
|
|
380
393
|
const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
381
394
|
expect(response.status).toBe(200);
|
|
382
|
-
expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' },
|
|
395
|
+
expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, authData);
|
|
383
396
|
});
|
|
384
397
|
it('should handle interaction request body without data wrapper', async () => {
|
|
398
|
+
const authData = {
|
|
399
|
+
provider: 'OptiID',
|
|
400
|
+
credentials: {
|
|
401
|
+
customer_id: 'test-customer',
|
|
402
|
+
instance_id: 'test-instance',
|
|
403
|
+
access_token: 'test-token',
|
|
404
|
+
product_sku: 'test-sku'
|
|
405
|
+
}
|
|
406
|
+
};
|
|
385
407
|
const interactionRequest = createMockRequest({
|
|
386
408
|
path: '/test-interaction',
|
|
387
|
-
bodyJSON: { param1: 'test-value' },
|
|
388
|
-
body: JSON.stringify({ param1: 'test-value' })
|
|
409
|
+
bodyJSON: { param1: 'test-value', auth: authData },
|
|
410
|
+
body: JSON.stringify({ param1: 'test-value', auth: authData })
|
|
389
411
|
});
|
|
390
412
|
const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
391
413
|
expect(response.status).toBe(200);
|
|
392
|
-
expect(mockInteraction.handler)
|
|
414
|
+
expect(mockInteraction.handler)
|
|
415
|
+
.toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value', auth: authData }, authData);
|
|
393
416
|
});
|
|
394
417
|
it('should execute interaction with OptiID auth data when provided', async () => {
|
|
395
|
-
const authData =
|
|
418
|
+
const authData = {
|
|
419
|
+
provider: 'OptiID',
|
|
420
|
+
credentials: {
|
|
421
|
+
customer_id: 'customer123',
|
|
422
|
+
instance_id: 'instance123',
|
|
423
|
+
access_token: 'token123',
|
|
424
|
+
product_sku: 'sku123'
|
|
425
|
+
}
|
|
426
|
+
};
|
|
396
427
|
const interactionRequest = createMockRequest({
|
|
397
428
|
path: '/test-interaction',
|
|
398
429
|
bodyJSON: {
|
|
@@ -410,7 +441,15 @@ describe('ToolsService', () => {
|
|
|
410
441
|
{ param1: 'test-value' }, authData);
|
|
411
442
|
});
|
|
412
443
|
it('should handle interaction request without data wrapper but with auth data', async () => {
|
|
413
|
-
const authData =
|
|
444
|
+
const authData = {
|
|
445
|
+
provider: 'OptiID',
|
|
446
|
+
credentials: {
|
|
447
|
+
customer_id: 'customer123',
|
|
448
|
+
instance_id: 'instance123',
|
|
449
|
+
access_token: 'token123',
|
|
450
|
+
product_sku: 'sku123'
|
|
451
|
+
}
|
|
452
|
+
};
|
|
414
453
|
const interactionRequest = createMockRequest({
|
|
415
454
|
path: '/test-interaction',
|
|
416
455
|
bodyJSON: {
|
|
@@ -430,48 +469,59 @@ describe('ToolsService', () => {
|
|
|
430
469
|
auth: authData
|
|
431
470
|
}, authData);
|
|
432
471
|
});
|
|
433
|
-
it('should
|
|
472
|
+
it('should throw error when interaction handler throws a regular error', async () => {
|
|
473
|
+
const authData = {
|
|
474
|
+
provider: 'OptiID',
|
|
475
|
+
credentials: {
|
|
476
|
+
customer_id: 'test-customer',
|
|
477
|
+
instance_id: 'test-instance',
|
|
478
|
+
access_token: 'test-token',
|
|
479
|
+
product_sku: 'test-sku'
|
|
480
|
+
}
|
|
481
|
+
};
|
|
434
482
|
const errorMessage = 'Interaction execution failed';
|
|
435
483
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
436
484
|
const interactionRequest = createMockRequest({
|
|
437
485
|
path: '/test-interaction',
|
|
438
|
-
bodyJSON: { data: { param1: 'test-value' } }
|
|
486
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData }
|
|
439
487
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
expect(response.bodyJSON).toEqual({
|
|
443
|
-
title: 'Internal Server Error',
|
|
444
|
-
status: 500,
|
|
445
|
-
detail: errorMessage,
|
|
446
|
-
instance: mockInteraction.endpoint
|
|
447
|
-
});
|
|
448
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
449
|
-
expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockInteraction.name}:`, expect.any(Error));
|
|
488
|
+
await expect(Service_1.toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
489
|
+
.rejects.toThrow(errorMessage);
|
|
450
490
|
});
|
|
451
|
-
it('should
|
|
491
|
+
it('should throw ToolError when interaction handler throws ToolError', async () => {
|
|
492
|
+
const authData = {
|
|
493
|
+
provider: 'OptiID',
|
|
494
|
+
credentials: {
|
|
495
|
+
customer_id: 'test-customer',
|
|
496
|
+
instance_id: 'test-instance',
|
|
497
|
+
access_token: 'test-token',
|
|
498
|
+
product_sku: 'test-sku'
|
|
499
|
+
}
|
|
500
|
+
};
|
|
452
501
|
const toolError = new ToolError_1.ToolError('Webhook validation failed', 400, 'Invalid signature');
|
|
453
502
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(toolError);
|
|
454
503
|
const interactionRequest = createMockRequest({
|
|
455
504
|
path: '/test-interaction',
|
|
456
|
-
bodyJSON: { data: { param1: 'test-value' } }
|
|
457
|
-
});
|
|
458
|
-
const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
459
|
-
expect(response.status).toBe(400);
|
|
460
|
-
expect(response.bodyJSON).toEqual({
|
|
461
|
-
title: 'Webhook validation failed',
|
|
462
|
-
status: 400,
|
|
463
|
-
detail: 'Invalid signature',
|
|
464
|
-
instance: mockInteraction.endpoint
|
|
505
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData }
|
|
465
506
|
});
|
|
466
|
-
expect(
|
|
467
|
-
|
|
507
|
+
await expect(Service_1.toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
508
|
+
.rejects.toThrow(toolError);
|
|
468
509
|
});
|
|
469
510
|
});
|
|
470
511
|
describe('error cases', () => {
|
|
471
|
-
it('should
|
|
512
|
+
it('should throw ToolError with 404 when no matching tool or interaction is found', async () => {
|
|
472
513
|
const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
|
|
473
|
-
|
|
474
|
-
|
|
514
|
+
await expect(Service_1.toolsService.processRequest(unknownRequest, mockToolFunction))
|
|
515
|
+
.rejects.toThrow(ToolError_1.ToolError);
|
|
516
|
+
try {
|
|
517
|
+
await Service_1.toolsService.processRequest(unknownRequest, mockToolFunction);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
expect(error).toBeInstanceOf(ToolError_1.ToolError);
|
|
521
|
+
expect(error.status).toBe(404);
|
|
522
|
+
// ToolError prepends status to message
|
|
523
|
+
expect(error.message).toContain('Function not found');
|
|
524
|
+
}
|
|
475
525
|
});
|
|
476
526
|
it('should handle tool with OptiID auth requirements', async () => {
|
|
477
527
|
const authRequirements = [
|
|
@@ -486,7 +536,7 @@ describe('ToolsService', () => {
|
|
|
486
536
|
});
|
|
487
537
|
});
|
|
488
538
|
describe('edge cases', () => {
|
|
489
|
-
it('should
|
|
539
|
+
it('should throw 403 when request has null bodyJSON (no auth data)', async () => {
|
|
490
540
|
// Create a tool without required parameters
|
|
491
541
|
const toolWithoutRequiredParams = {
|
|
492
542
|
name: 'no_required_params_tool',
|
|
@@ -501,11 +551,17 @@ describe('ToolsService', () => {
|
|
|
501
551
|
bodyJSON: null,
|
|
502
552
|
body: null
|
|
503
553
|
});
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
554
|
+
await expect(Service_1.toolsService.processRequest(requestWithNullBody, mockToolFunction))
|
|
555
|
+
.rejects.toThrow(ToolError_1.ToolError);
|
|
556
|
+
try {
|
|
557
|
+
await Service_1.toolsService.processRequest(requestWithNullBody, mockToolFunction);
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
expect(error.status).toBe(403);
|
|
561
|
+
expect(error.message).toContain('Authentication data is required');
|
|
562
|
+
}
|
|
507
563
|
});
|
|
508
|
-
it('should
|
|
564
|
+
it('should throw 403 when request has undefined bodyJSON (no auth data)', async () => {
|
|
509
565
|
// Create a tool without required parameters
|
|
510
566
|
const toolWithoutRequiredParams = {
|
|
511
567
|
name: 'no_required_params_tool_2',
|
|
@@ -520,13 +576,27 @@ describe('ToolsService', () => {
|
|
|
520
576
|
bodyJSON: undefined,
|
|
521
577
|
body: undefined
|
|
522
578
|
});
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
579
|
+
await expect(Service_1.toolsService.processRequest(requestWithUndefinedBody, mockToolFunction))
|
|
580
|
+
.rejects.toThrow(ToolError_1.ToolError);
|
|
581
|
+
try {
|
|
582
|
+
await Service_1.toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
expect(error.status).toBe(403);
|
|
586
|
+
expect(error.message).toContain('Authentication data is required');
|
|
587
|
+
}
|
|
526
588
|
});
|
|
527
589
|
it('should extract auth data from bodyJSON when body exists', async () => {
|
|
528
590
|
Service_1.toolsService.registerTool(mockTool.name, mockTool.description, mockTool.handler, mockTool.parameters, mockTool.endpoint);
|
|
529
|
-
const authData =
|
|
591
|
+
const authData = {
|
|
592
|
+
provider: 'OptiID',
|
|
593
|
+
credentials: {
|
|
594
|
+
customer_id: 'customer123',
|
|
595
|
+
instance_id: 'instance123',
|
|
596
|
+
access_token: 'token123',
|
|
597
|
+
product_sku: 'sku123'
|
|
598
|
+
}
|
|
599
|
+
};
|
|
530
600
|
const requestWithAuth = createMockRequest({
|
|
531
601
|
bodyJSON: {
|
|
532
602
|
parameters: { param1: 'test-value' },
|
|
@@ -542,7 +612,7 @@ describe('ToolsService', () => {
|
|
|
542
612
|
expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, // functionContext
|
|
543
613
|
{ param1: 'test-value' }, authData);
|
|
544
614
|
});
|
|
545
|
-
it('should
|
|
615
|
+
it('should throw 403 when auth data is missing for tool with auth requirements', async () => {
|
|
546
616
|
Service_1.toolsService.registerTool(mockTool.name, mockTool.description, mockTool.handler, mockTool.parameters, mockTool.endpoint);
|
|
547
617
|
const requestWithoutAuth = createMockRequest({
|
|
548
618
|
bodyJSON: {
|
|
@@ -553,14 +623,27 @@ describe('ToolsService', () => {
|
|
|
553
623
|
parameters: { param1: 'test-value' }
|
|
554
624
|
})
|
|
555
625
|
});
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
626
|
+
await expect(Service_1.toolsService.processRequest(requestWithoutAuth, mockToolFunction))
|
|
627
|
+
.rejects.toThrow(ToolError_1.ToolError);
|
|
628
|
+
try {
|
|
629
|
+
await Service_1.toolsService.processRequest(requestWithoutAuth, mockToolFunction);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
expect(error.status).toBe(403);
|
|
633
|
+
expect(error.message).toContain('Authentication data is required');
|
|
634
|
+
}
|
|
560
635
|
});
|
|
561
636
|
it('should handle auth extraction when body is falsy but bodyJSON has auth', async () => {
|
|
562
637
|
Service_1.toolsService.registerTool(mockTool.name, mockTool.description, mockTool.handler, mockTool.parameters, mockTool.endpoint);
|
|
563
|
-
const authData =
|
|
638
|
+
const authData = {
|
|
639
|
+
provider: 'OptiID',
|
|
640
|
+
credentials: {
|
|
641
|
+
customer_id: 'customer123',
|
|
642
|
+
instance_id: 'instance123',
|
|
643
|
+
access_token: 'token123',
|
|
644
|
+
product_sku: 'sku123'
|
|
645
|
+
}
|
|
646
|
+
};
|
|
564
647
|
const requestWithAuthButNoBody = createMockRequest({
|
|
565
648
|
bodyJSON: {
|
|
566
649
|
parameters: { param1: 'test-value' },
|
|
@@ -578,7 +661,7 @@ describe('ToolsService', () => {
|
|
|
578
661
|
beforeEach(() => {
|
|
579
662
|
jest.clearAllMocks();
|
|
580
663
|
});
|
|
581
|
-
it('should
|
|
664
|
+
it('should throw ToolError with 400 for invalid parameter types', async () => {
|
|
582
665
|
// Register a tool with specific parameter types
|
|
583
666
|
const toolWithTypedParams = {
|
|
584
667
|
name: 'typed_tool',
|
|
@@ -603,22 +686,25 @@ describe('ToolsService', () => {
|
|
|
603
686
|
}
|
|
604
687
|
}
|
|
605
688
|
});
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
689
|
+
await expect(Service_1.toolsService.processRequest(invalidRequest, mockToolFunction))
|
|
690
|
+
.rejects.toThrow(ToolError_1.ToolError);
|
|
691
|
+
try {
|
|
692
|
+
await Service_1.toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
expect(error).toBeInstanceOf(ToolError_1.ToolError);
|
|
696
|
+
const toolError = error;
|
|
697
|
+
expect(toolError.status).toBe(400);
|
|
698
|
+
// The ToolError has title property, message includes more details
|
|
699
|
+
expect(toolError.toProblemDetails('/typed-tool')).toMatchObject({
|
|
700
|
+
title: 'One or more validation errors occurred.',
|
|
701
|
+
status: 400
|
|
702
|
+
});
|
|
703
|
+
expect(toolError.errors).toHaveLength(3);
|
|
704
|
+
expect(toolError.errors[0]).toHaveProperty('field', 'name');
|
|
705
|
+
expect(toolError.errors[0].message)
|
|
706
|
+
.toBe("Parameter 'name' must be a string, but received number");
|
|
707
|
+
}
|
|
622
708
|
// Verify the handler was not called
|
|
623
709
|
expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
|
|
624
710
|
});
|
|
@@ -631,18 +717,28 @@ describe('ToolsService', () => {
|
|
|
631
717
|
endpoint: '/no-params-tool'
|
|
632
718
|
};
|
|
633
719
|
Service_1.toolsService.registerTool(toolWithoutParams.name, toolWithoutParams.description, toolWithoutParams.handler, toolWithoutParams.parameters, toolWithoutParams.endpoint);
|
|
720
|
+
const authData = {
|
|
721
|
+
provider: 'OptiID',
|
|
722
|
+
credentials: {
|
|
723
|
+
customer_id: 'test-customer',
|
|
724
|
+
instance_id: 'test-instance',
|
|
725
|
+
access_token: 'test-token',
|
|
726
|
+
product_sku: 'test-sku'
|
|
727
|
+
}
|
|
728
|
+
};
|
|
634
729
|
// Send request with any data (should be ignored)
|
|
635
730
|
const request = createMockRequest({
|
|
636
731
|
path: '/no-params-tool',
|
|
637
732
|
bodyJSON: {
|
|
638
733
|
parameters: {
|
|
639
734
|
unexpected: 'value'
|
|
640
|
-
}
|
|
735
|
+
},
|
|
736
|
+
auth: authData
|
|
641
737
|
}
|
|
642
738
|
});
|
|
643
739
|
const response = await Service_1.toolsService.processRequest(request, mockToolFunction);
|
|
644
740
|
expect(response.status).toBe(200);
|
|
645
|
-
expect(toolWithoutParams.handler).toHaveBeenCalledWith(mockToolFunction, { unexpected: 'value' },
|
|
741
|
+
expect(toolWithoutParams.handler).toHaveBeenCalledWith(mockToolFunction, { unexpected: 'value' }, authData);
|
|
646
742
|
});
|
|
647
743
|
});
|
|
648
744
|
});
|