@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
2
2
|
import { toolsService, Tool, Interaction } from './Service';
|
|
3
|
-
import { Parameter, ParameterType, AuthRequirement,
|
|
3
|
+
import { Parameter, ParameterType, AuthRequirement, AuthData } from '../types/Models';
|
|
4
4
|
import { ToolError } from '../types/ToolError';
|
|
5
5
|
import { ToolFunction } from '../function/ToolFunction';
|
|
6
6
|
import { logger } from '@zaiusinc/app-sdk';
|
|
@@ -71,14 +71,19 @@ jest.mock('@zaiusinc/app-sdk', () => {
|
|
|
71
71
|
};
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// Mock the authenticateInternalRequest function
|
|
75
|
+
jest.mock('../auth/AuthUtils', () => ({
|
|
76
|
+
authenticateInternalRequest: jest.fn().mockResolvedValue(undefined)
|
|
77
|
+
}));
|
|
78
|
+
|
|
74
79
|
// Get the mocked kvStore for use in tests
|
|
75
80
|
const { storage } = jest.requireMock('@zaiusinc/app-sdk');
|
|
76
81
|
const mockKvStore = storage.kvStore;
|
|
77
82
|
|
|
78
83
|
|
|
79
84
|
describe('ToolsService', () => {
|
|
80
|
-
let mockTool: Tool
|
|
81
|
-
let mockInteraction: Interaction
|
|
85
|
+
let mockTool: Tool;
|
|
86
|
+
let mockInteraction: Interaction;
|
|
82
87
|
let mockToolFunction: ToolFunction;
|
|
83
88
|
|
|
84
89
|
beforeEach(() => {
|
|
@@ -130,11 +135,21 @@ describe('ToolsService', () => {
|
|
|
130
135
|
return map;
|
|
131
136
|
};
|
|
132
137
|
|
|
138
|
+
const defaultAuth = {
|
|
139
|
+
provider: 'OptiID',
|
|
140
|
+
credentials: {
|
|
141
|
+
customer_id: 'test-customer',
|
|
142
|
+
instance_id: 'test-instance',
|
|
143
|
+
access_token: 'test-token',
|
|
144
|
+
product_sku: 'test-sku'
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
133
148
|
const baseRequest = {
|
|
134
149
|
path: '/test-tool',
|
|
135
150
|
method: 'POST',
|
|
136
|
-
bodyJSON: { parameters: { param1: 'test-value' } },
|
|
137
|
-
body: JSON.stringify({ parameters: { param1: 'test-value' } }),
|
|
151
|
+
bodyJSON: { parameters: { param1: 'test-value' }, auth: defaultAuth },
|
|
152
|
+
body: JSON.stringify({ parameters: { param1: 'test-value' }, auth: defaultAuth }),
|
|
138
153
|
bodyData: Buffer.from(''),
|
|
139
154
|
headers: createHeadersMap(),
|
|
140
155
|
params: {},
|
|
@@ -250,8 +265,7 @@ describe('ToolsService', () => {
|
|
|
250
265
|
endpoint: '/second-tool',
|
|
251
266
|
http_method: 'POST',
|
|
252
267
|
auth_requirements: [
|
|
253
|
-
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
254
|
-
{ provider: 'OptiID', scope_bundle: 'default', required: true }
|
|
268
|
+
{ provider: 'oauth2', scope_bundle: 'calendar', required: true }
|
|
255
269
|
]
|
|
256
270
|
});
|
|
257
271
|
});
|
|
@@ -276,7 +290,7 @@ describe('ToolsService', () => {
|
|
|
276
290
|
expect(mockTool.handler).toHaveBeenCalledWith(
|
|
277
291
|
mockToolFunction, // functionContext
|
|
278
292
|
{ param1: 'test-value' },
|
|
279
|
-
|
|
293
|
+
expect.objectContaining({ provider: 'OptiID' })
|
|
280
294
|
);
|
|
281
295
|
});
|
|
282
296
|
|
|
@@ -298,7 +312,7 @@ describe('ToolsService', () => {
|
|
|
298
312
|
expect(mockTool.handler).toHaveBeenCalledWith(
|
|
299
313
|
mockToolFunctionInstance, // functionContext - existing instance
|
|
300
314
|
{ param1: 'test-value' },
|
|
301
|
-
|
|
315
|
+
expect.objectContaining({ provider: 'OptiID' })
|
|
302
316
|
);
|
|
303
317
|
});
|
|
304
318
|
|
|
@@ -345,9 +359,19 @@ describe('ToolsService', () => {
|
|
|
345
359
|
'/test-toolfunction-access'
|
|
346
360
|
);
|
|
347
361
|
|
|
362
|
+
const authData = {
|
|
363
|
+
provider: 'OptiID',
|
|
364
|
+
credentials: {
|
|
365
|
+
customer_id: 'test-customer',
|
|
366
|
+
instance_id: 'test-instance',
|
|
367
|
+
access_token: 'test-token',
|
|
368
|
+
product_sku: 'test-sku'
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
348
372
|
const testRequest = createMockRequest({
|
|
349
373
|
path: '/test-toolfunction-access',
|
|
350
|
-
bodyJSON: { action: 'test' }
|
|
374
|
+
bodyJSON: { action: 'test', auth: authData }
|
|
351
375
|
});
|
|
352
376
|
|
|
353
377
|
const response = await toolsService.processRequest(testRequest, mockToolFunctionInstance);
|
|
@@ -357,19 +381,24 @@ describe('ToolsService', () => {
|
|
|
357
381
|
expect((response as any).data.success).toBe(true);
|
|
358
382
|
expect((response as any).data.requestPath).toBe('/test-path');
|
|
359
383
|
expect((response as any).data.testMethodResult).toBe('path: /test-path');
|
|
360
|
-
expect((response as any).data.receivedParams).toEqual({ action: 'test' });
|
|
384
|
+
expect((response as any).data.receivedParams).toEqual({ action: 'test', auth: authData });
|
|
361
385
|
expect(handlerThatAccessesRequest).toHaveBeenCalledWith(
|
|
362
386
|
mockToolFunctionInstance, // functionContext is the ToolFunction instance
|
|
363
|
-
{ action: 'test' },
|
|
364
|
-
|
|
387
|
+
{ action: 'test', auth: authData },
|
|
388
|
+
authData
|
|
365
389
|
);
|
|
366
390
|
});
|
|
367
391
|
|
|
368
392
|
it('should execute tool with OptiID auth data when provided', async () => {
|
|
369
|
-
const authData =
|
|
370
|
-
'
|
|
371
|
-
|
|
372
|
-
|
|
393
|
+
const authData: AuthData = {
|
|
394
|
+
provider: 'OptiID',
|
|
395
|
+
credentials: {
|
|
396
|
+
customer_id: 'customer123',
|
|
397
|
+
instance_id: 'instance123',
|
|
398
|
+
access_token: 'token123',
|
|
399
|
+
product_sku: 'sku123'
|
|
400
|
+
}
|
|
401
|
+
};
|
|
373
402
|
|
|
374
403
|
const requestWithAuth = createMockRequest({
|
|
375
404
|
bodyJSON: {
|
|
@@ -393,9 +422,19 @@ describe('ToolsService', () => {
|
|
|
393
422
|
});
|
|
394
423
|
|
|
395
424
|
it('should handle request body without parameters wrapper', async () => {
|
|
425
|
+
const authData = {
|
|
426
|
+
provider: 'OptiID',
|
|
427
|
+
credentials: {
|
|
428
|
+
customer_id: 'test-customer',
|
|
429
|
+
instance_id: 'test-instance',
|
|
430
|
+
access_token: 'test-token',
|
|
431
|
+
product_sku: 'test-sku'
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
396
435
|
const requestWithoutWrapper = createMockRequest({
|
|
397
|
-
bodyJSON: { param1: 'test-value' },
|
|
398
|
-
body: JSON.stringify({ param1: 'test-value' })
|
|
436
|
+
bodyJSON: { param1: 'test-value', auth: authData },
|
|
437
|
+
body: JSON.stringify({ param1: 'test-value', auth: authData })
|
|
399
438
|
});
|
|
400
439
|
|
|
401
440
|
const response = await toolsService.processRequest(requestWithoutWrapper, mockToolFunction);
|
|
@@ -403,100 +442,60 @@ describe('ToolsService', () => {
|
|
|
403
442
|
expect(response.status).toBe(200);
|
|
404
443
|
expect(mockTool.handler).toHaveBeenCalledWith(
|
|
405
444
|
mockToolFunction, // functionContext
|
|
406
|
-
{ param1: 'test-value' },
|
|
407
|
-
|
|
445
|
+
{ param1: 'test-value', auth: authData },
|
|
446
|
+
authData
|
|
408
447
|
);
|
|
409
448
|
});
|
|
410
449
|
|
|
411
|
-
it('should
|
|
450
|
+
it('should throw error when tool handler throws a regular error', async () => {
|
|
412
451
|
const errorMessage = 'Tool execution failed';
|
|
413
452
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
414
453
|
|
|
415
454
|
const mockRequest = createMockRequest();
|
|
416
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
417
455
|
|
|
418
|
-
expect(
|
|
419
|
-
|
|
420
|
-
title: 'Internal Server Error',
|
|
421
|
-
status: 500,
|
|
422
|
-
detail: errorMessage,
|
|
423
|
-
instance: mockTool.endpoint
|
|
424
|
-
});
|
|
425
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
426
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
427
|
-
`Error in function ${mockTool.name}:`,
|
|
428
|
-
expect.any(Error)
|
|
429
|
-
);
|
|
456
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
457
|
+
.rejects.toThrow(errorMessage);
|
|
430
458
|
});
|
|
431
459
|
|
|
432
|
-
it('should
|
|
460
|
+
it('should throw when tool handler throws object without message', async () => {
|
|
433
461
|
jest.mocked(mockTool.handler).mockRejectedValueOnce({});
|
|
434
462
|
|
|
435
463
|
const mockRequest = createMockRequest();
|
|
436
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
437
464
|
|
|
438
|
-
expect(
|
|
439
|
-
|
|
440
|
-
title: 'Internal Server Error',
|
|
441
|
-
status: 500,
|
|
442
|
-
detail: 'An unexpected error occurred',
|
|
443
|
-
instance: mockTool.endpoint
|
|
444
|
-
});
|
|
445
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
465
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
466
|
+
.rejects.toEqual({});
|
|
446
467
|
});
|
|
447
468
|
|
|
448
|
-
it('should
|
|
469
|
+
it('should throw ToolError when tool handler throws ToolError', async () => {
|
|
449
470
|
const toolError = new ToolError('Resource not found', 404, 'The requested task does not exist');
|
|
450
471
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
451
472
|
|
|
452
473
|
const mockRequest = createMockRequest();
|
|
453
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
454
474
|
|
|
455
|
-
expect(
|
|
456
|
-
|
|
457
|
-
title: 'Resource not found',
|
|
458
|
-
status: 404,
|
|
459
|
-
detail: 'The requested task does not exist',
|
|
460
|
-
instance: mockTool.endpoint
|
|
461
|
-
});
|
|
462
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
463
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
464
|
-
`Error in function ${mockTool.name}:`,
|
|
465
|
-
expect.any(ToolError)
|
|
466
|
-
);
|
|
475
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
476
|
+
.rejects.toThrow(toolError);
|
|
467
477
|
});
|
|
468
478
|
|
|
469
|
-
it('should
|
|
479
|
+
it('should throw ToolError without detail when detail is not provided', async () => {
|
|
470
480
|
const toolError = new ToolError('Bad request', 400);
|
|
471
481
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
472
482
|
|
|
473
483
|
const mockRequest = createMockRequest();
|
|
474
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
475
484
|
|
|
476
|
-
expect(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
status: 400,
|
|
480
|
-
instance: mockTool.endpoint
|
|
481
|
-
});
|
|
482
|
-
expect(response.bodyJSON).not.toHaveProperty('detail');
|
|
483
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
485
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
486
|
+
.rejects.toThrow(toolError);
|
|
487
|
+
expect(toolError.status).toBe(400);
|
|
484
488
|
});
|
|
485
489
|
|
|
486
|
-
it('should default
|
|
490
|
+
it('should throw ToolError with default 500 status when created without status', async () => {
|
|
487
491
|
const toolError = new ToolError('Database error');
|
|
488
492
|
jest.mocked(mockTool.handler).mockRejectedValueOnce(toolError);
|
|
489
493
|
|
|
490
494
|
const mockRequest = createMockRequest();
|
|
491
|
-
const response = await toolsService.processRequest(mockRequest, mockToolFunction);
|
|
492
495
|
|
|
493
|
-
expect(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
status: 500,
|
|
497
|
-
instance: mockTool.endpoint
|
|
498
|
-
});
|
|
499
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
496
|
+
await expect(toolsService.processRequest(mockRequest, mockToolFunction))
|
|
497
|
+
.rejects.toThrow(toolError);
|
|
498
|
+
expect(toolError.status).toBe(500);
|
|
500
499
|
});
|
|
501
500
|
});
|
|
502
501
|
|
|
@@ -510,36 +509,62 @@ describe('ToolsService', () => {
|
|
|
510
509
|
});
|
|
511
510
|
|
|
512
511
|
it('should execute interaction successfully with data', async () => {
|
|
512
|
+
const authData = {
|
|
513
|
+
provider: 'OptiID',
|
|
514
|
+
credentials: {
|
|
515
|
+
customer_id: 'test-customer',
|
|
516
|
+
instance_id: 'test-instance',
|
|
517
|
+
access_token: 'test-token',
|
|
518
|
+
product_sku: 'test-sku'
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
513
522
|
const interactionRequest = createMockRequest({
|
|
514
523
|
path: '/test-interaction',
|
|
515
|
-
bodyJSON: { data: { param1: 'test-value' } },
|
|
516
|
-
body: JSON.stringify({ data: { param1: 'test-value' } })
|
|
524
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData },
|
|
525
|
+
body: JSON.stringify({ data: { param1: 'test-value' }, auth: authData })
|
|
517
526
|
});
|
|
518
527
|
|
|
519
528
|
const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
520
529
|
|
|
521
530
|
expect(response.status).toBe(200);
|
|
522
|
-
expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' },
|
|
531
|
+
expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, authData);
|
|
523
532
|
});
|
|
524
533
|
|
|
525
534
|
it('should handle interaction request body without data wrapper', async () => {
|
|
535
|
+
const authData = {
|
|
536
|
+
provider: 'OptiID',
|
|
537
|
+
credentials: {
|
|
538
|
+
customer_id: 'test-customer',
|
|
539
|
+
instance_id: 'test-instance',
|
|
540
|
+
access_token: 'test-token',
|
|
541
|
+
product_sku: 'test-sku'
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
526
545
|
const interactionRequest = createMockRequest({
|
|
527
546
|
path: '/test-interaction',
|
|
528
|
-
bodyJSON: { param1: 'test-value' },
|
|
529
|
-
body: JSON.stringify({ param1: 'test-value' })
|
|
547
|
+
bodyJSON: { param1: 'test-value', auth: authData },
|
|
548
|
+
body: JSON.stringify({ param1: 'test-value', auth: authData })
|
|
530
549
|
});
|
|
531
550
|
|
|
532
551
|
const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
|
|
533
552
|
|
|
534
553
|
expect(response.status).toBe(200);
|
|
535
|
-
expect(mockInteraction.handler)
|
|
554
|
+
expect(mockInteraction.handler)
|
|
555
|
+
.toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value', auth: authData }, authData);
|
|
536
556
|
});
|
|
537
557
|
|
|
538
558
|
it('should execute interaction with OptiID auth data when provided', async () => {
|
|
539
|
-
const authData =
|
|
540
|
-
'
|
|
541
|
-
|
|
542
|
-
|
|
559
|
+
const authData = {
|
|
560
|
+
provider: 'OptiID',
|
|
561
|
+
credentials: {
|
|
562
|
+
customer_id: 'customer123',
|
|
563
|
+
instance_id: 'instance123',
|
|
564
|
+
access_token: 'token123',
|
|
565
|
+
product_sku: 'sku123'
|
|
566
|
+
}
|
|
567
|
+
};
|
|
543
568
|
|
|
544
569
|
const interactionRequest = createMockRequest({
|
|
545
570
|
path: '/test-interaction',
|
|
@@ -564,10 +589,15 @@ describe('ToolsService', () => {
|
|
|
564
589
|
});
|
|
565
590
|
|
|
566
591
|
it('should handle interaction request without data wrapper but with auth data', async () => {
|
|
567
|
-
const authData =
|
|
568
|
-
'
|
|
569
|
-
|
|
570
|
-
|
|
592
|
+
const authData = {
|
|
593
|
+
provider: 'OptiID',
|
|
594
|
+
credentials: {
|
|
595
|
+
customer_id: 'customer123',
|
|
596
|
+
instance_id: 'instance123',
|
|
597
|
+
access_token: 'token123',
|
|
598
|
+
product_sku: 'sku123'
|
|
599
|
+
}
|
|
600
|
+
};
|
|
571
601
|
|
|
572
602
|
const interactionRequest = createMockRequest({
|
|
573
603
|
path: '/test-interaction',
|
|
@@ -594,63 +624,66 @@ describe('ToolsService', () => {
|
|
|
594
624
|
);
|
|
595
625
|
});
|
|
596
626
|
|
|
597
|
-
it('should
|
|
627
|
+
it('should throw error when interaction handler throws a regular error', async () => {
|
|
628
|
+
const authData = {
|
|
629
|
+
provider: 'OptiID',
|
|
630
|
+
credentials: {
|
|
631
|
+
customer_id: 'test-customer',
|
|
632
|
+
instance_id: 'test-instance',
|
|
633
|
+
access_token: 'test-token',
|
|
634
|
+
product_sku: 'test-sku'
|
|
635
|
+
}
|
|
636
|
+
};
|
|
598
637
|
const errorMessage = 'Interaction execution failed';
|
|
599
638
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(new Error(errorMessage));
|
|
600
639
|
|
|
601
640
|
const interactionRequest = createMockRequest({
|
|
602
641
|
path: '/test-interaction',
|
|
603
|
-
bodyJSON: { data: { param1: 'test-value' } }
|
|
642
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData }
|
|
604
643
|
});
|
|
605
644
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
expect(response.status).toBe(500);
|
|
609
|
-
expect(response.bodyJSON).toEqual({
|
|
610
|
-
title: 'Internal Server Error',
|
|
611
|
-
status: 500,
|
|
612
|
-
detail: errorMessage,
|
|
613
|
-
instance: mockInteraction.endpoint
|
|
614
|
-
});
|
|
615
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
616
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
617
|
-
`Error in function ${mockInteraction.name}:`,
|
|
618
|
-
expect.any(Error)
|
|
619
|
-
);
|
|
645
|
+
await expect(toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
646
|
+
.rejects.toThrow(errorMessage);
|
|
620
647
|
});
|
|
621
648
|
|
|
622
|
-
it('should
|
|
649
|
+
it('should throw ToolError when interaction handler throws ToolError', async () => {
|
|
650
|
+
const authData = {
|
|
651
|
+
provider: 'OptiID',
|
|
652
|
+
credentials: {
|
|
653
|
+
customer_id: 'test-customer',
|
|
654
|
+
instance_id: 'test-instance',
|
|
655
|
+
access_token: 'test-token',
|
|
656
|
+
product_sku: 'test-sku'
|
|
657
|
+
}
|
|
658
|
+
};
|
|
623
659
|
const toolError = new ToolError('Webhook validation failed', 400, 'Invalid signature');
|
|
624
660
|
jest.mocked(mockInteraction.handler).mockRejectedValueOnce(toolError);
|
|
625
661
|
|
|
626
662
|
const interactionRequest = createMockRequest({
|
|
627
663
|
path: '/test-interaction',
|
|
628
|
-
bodyJSON: { data: { param1: 'test-value' } }
|
|
664
|
+
bodyJSON: { data: { param1: 'test-value' }, auth: authData }
|
|
629
665
|
});
|
|
630
666
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
expect(response.status).toBe(400);
|
|
634
|
-
expect(response.bodyJSON).toEqual({
|
|
635
|
-
title: 'Webhook validation failed',
|
|
636
|
-
status: 400,
|
|
637
|
-
detail: 'Invalid signature',
|
|
638
|
-
instance: mockInteraction.endpoint
|
|
639
|
-
});
|
|
640
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
641
|
-
expect(logger.error).toHaveBeenCalledWith(
|
|
642
|
-
`Error in function ${mockInteraction.name}:`,
|
|
643
|
-
expect.any(ToolError)
|
|
644
|
-
);
|
|
667
|
+
await expect(toolsService.processRequest(interactionRequest, mockToolFunction))
|
|
668
|
+
.rejects.toThrow(toolError);
|
|
645
669
|
});
|
|
646
670
|
});
|
|
647
671
|
|
|
648
672
|
describe('error cases', () => {
|
|
649
|
-
it('should
|
|
673
|
+
it('should throw ToolError with 404 when no matching tool or interaction is found', async () => {
|
|
650
674
|
const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
|
|
651
|
-
const response = await toolsService.processRequest(unknownRequest, mockToolFunction);
|
|
652
675
|
|
|
653
|
-
expect(
|
|
676
|
+
await expect(toolsService.processRequest(unknownRequest, mockToolFunction))
|
|
677
|
+
.rejects.toThrow(ToolError);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
await toolsService.processRequest(unknownRequest, mockToolFunction);
|
|
681
|
+
} catch (error) {
|
|
682
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
683
|
+
expect((error as ToolError).status).toBe(404);
|
|
684
|
+
// ToolError prepends status to message
|
|
685
|
+
expect((error as ToolError).message).toContain('Function not found');
|
|
686
|
+
}
|
|
654
687
|
});
|
|
655
688
|
|
|
656
689
|
it('should handle tool with OptiID auth requirements', async () => {
|
|
@@ -678,7 +711,7 @@ describe('ToolsService', () => {
|
|
|
678
711
|
});
|
|
679
712
|
|
|
680
713
|
describe('edge cases', () => {
|
|
681
|
-
it('should
|
|
714
|
+
it('should throw 403 when request has null bodyJSON (no auth data)', async () => {
|
|
682
715
|
// Create a tool without required parameters
|
|
683
716
|
const toolWithoutRequiredParams = {
|
|
684
717
|
name: 'no_required_params_tool',
|
|
@@ -702,13 +735,18 @@ describe('ToolsService', () => {
|
|
|
702
735
|
body: null
|
|
703
736
|
});
|
|
704
737
|
|
|
705
|
-
|
|
738
|
+
await expect(toolsService.processRequest(requestWithNullBody, mockToolFunction))
|
|
739
|
+
.rejects.toThrow(ToolError);
|
|
706
740
|
|
|
707
|
-
|
|
708
|
-
|
|
741
|
+
try {
|
|
742
|
+
await toolsService.processRequest(requestWithNullBody, mockToolFunction);
|
|
743
|
+
} catch (error) {
|
|
744
|
+
expect((error as ToolError).status).toBe(403);
|
|
745
|
+
expect((error as ToolError).message).toContain('Authentication data is required');
|
|
746
|
+
}
|
|
709
747
|
});
|
|
710
748
|
|
|
711
|
-
it('should
|
|
749
|
+
it('should throw 403 when request has undefined bodyJSON (no auth data)', async () => {
|
|
712
750
|
// Create a tool without required parameters
|
|
713
751
|
const toolWithoutRequiredParams = {
|
|
714
752
|
name: 'no_required_params_tool_2',
|
|
@@ -732,10 +770,15 @@ describe('ToolsService', () => {
|
|
|
732
770
|
body: undefined
|
|
733
771
|
});
|
|
734
772
|
|
|
735
|
-
|
|
773
|
+
await expect(toolsService.processRequest(requestWithUndefinedBody, mockToolFunction))
|
|
774
|
+
.rejects.toThrow(ToolError);
|
|
736
775
|
|
|
737
|
-
|
|
738
|
-
|
|
776
|
+
try {
|
|
777
|
+
await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
expect((error as ToolError).status).toBe(403);
|
|
780
|
+
expect((error as ToolError).message).toContain('Authentication data is required');
|
|
781
|
+
}
|
|
739
782
|
});
|
|
740
783
|
|
|
741
784
|
it('should extract auth data from bodyJSON when body exists', async () => {
|
|
@@ -747,10 +790,15 @@ describe('ToolsService', () => {
|
|
|
747
790
|
mockTool.endpoint
|
|
748
791
|
);
|
|
749
792
|
|
|
750
|
-
const authData =
|
|
751
|
-
'
|
|
752
|
-
|
|
753
|
-
|
|
793
|
+
const authData = {
|
|
794
|
+
provider: 'OptiID',
|
|
795
|
+
credentials: {
|
|
796
|
+
customer_id: 'customer123',
|
|
797
|
+
instance_id: 'instance123',
|
|
798
|
+
access_token: 'token123',
|
|
799
|
+
product_sku: 'sku123'
|
|
800
|
+
}
|
|
801
|
+
};
|
|
754
802
|
|
|
755
803
|
const requestWithAuth = createMockRequest({
|
|
756
804
|
bodyJSON: {
|
|
@@ -773,7 +821,7 @@ describe('ToolsService', () => {
|
|
|
773
821
|
);
|
|
774
822
|
});
|
|
775
823
|
|
|
776
|
-
it('should
|
|
824
|
+
it('should throw 403 when auth data is missing for tool with auth requirements', async () => {
|
|
777
825
|
toolsService.registerTool(
|
|
778
826
|
mockTool.name,
|
|
779
827
|
mockTool.description,
|
|
@@ -792,14 +840,15 @@ describe('ToolsService', () => {
|
|
|
792
840
|
})
|
|
793
841
|
});
|
|
794
842
|
|
|
795
|
-
|
|
843
|
+
await expect(toolsService.processRequest(requestWithoutAuth, mockToolFunction))
|
|
844
|
+
.rejects.toThrow(ToolError);
|
|
796
845
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
846
|
+
try {
|
|
847
|
+
await toolsService.processRequest(requestWithoutAuth, mockToolFunction);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
expect((error as ToolError).status).toBe(403);
|
|
850
|
+
expect((error as ToolError).message).toContain('Authentication data is required');
|
|
851
|
+
}
|
|
803
852
|
});
|
|
804
853
|
|
|
805
854
|
it('should handle auth extraction when body is falsy but bodyJSON has auth', async () => {
|
|
@@ -811,10 +860,15 @@ describe('ToolsService', () => {
|
|
|
811
860
|
mockTool.endpoint
|
|
812
861
|
);
|
|
813
862
|
|
|
814
|
-
const authData =
|
|
815
|
-
'
|
|
816
|
-
|
|
817
|
-
|
|
863
|
+
const authData = {
|
|
864
|
+
provider: 'OptiID',
|
|
865
|
+
credentials: {
|
|
866
|
+
customer_id: 'customer123',
|
|
867
|
+
instance_id: 'instance123',
|
|
868
|
+
access_token: 'token123',
|
|
869
|
+
product_sku: 'sku123'
|
|
870
|
+
}
|
|
871
|
+
};
|
|
818
872
|
|
|
819
873
|
const requestWithAuthButNoBody = createMockRequest({
|
|
820
874
|
bodyJSON: {
|
|
@@ -840,7 +894,7 @@ describe('ToolsService', () => {
|
|
|
840
894
|
jest.clearAllMocks();
|
|
841
895
|
});
|
|
842
896
|
|
|
843
|
-
it('should
|
|
897
|
+
it('should throw ToolError with 400 for invalid parameter types', async () => {
|
|
844
898
|
// Register a tool with specific parameter types
|
|
845
899
|
const toolWithTypedParams = {
|
|
846
900
|
name: 'typed_tool',
|
|
@@ -874,26 +928,25 @@ describe('ToolsService', () => {
|
|
|
874
928
|
}
|
|
875
929
|
});
|
|
876
930
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
expect(response.headers.get('content-type')).toBe('application/problem+json');
|
|
931
|
+
await expect(toolsService.processRequest(invalidRequest, mockToolFunction))
|
|
932
|
+
.rejects.toThrow(ToolError);
|
|
933
|
+
|
|
934
|
+
try {
|
|
935
|
+
await toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
expect(error).toBeInstanceOf(ToolError);
|
|
938
|
+
const toolError = error as ToolError;
|
|
939
|
+
expect(toolError.status).toBe(400);
|
|
940
|
+
// The ToolError has title property, message includes more details
|
|
941
|
+
expect(toolError.toProblemDetails('/typed-tool')).toMatchObject({
|
|
942
|
+
title: 'One or more validation errors occurred.',
|
|
943
|
+
status: 400
|
|
944
|
+
});
|
|
945
|
+
expect(toolError.errors).toHaveLength(3);
|
|
946
|
+
expect(toolError.errors![0]).toHaveProperty('field', 'name');
|
|
947
|
+
expect(toolError.errors![0].message)
|
|
948
|
+
.toBe("Parameter 'name' must be a string, but received number");
|
|
949
|
+
}
|
|
897
950
|
|
|
898
951
|
// Verify the handler was not called
|
|
899
952
|
expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
|
|
@@ -916,13 +969,24 @@ describe('ToolsService', () => {
|
|
|
916
969
|
toolWithoutParams.endpoint
|
|
917
970
|
);
|
|
918
971
|
|
|
972
|
+
const authData = {
|
|
973
|
+
provider: 'OptiID',
|
|
974
|
+
credentials: {
|
|
975
|
+
customer_id: 'test-customer',
|
|
976
|
+
instance_id: 'test-instance',
|
|
977
|
+
access_token: 'test-token',
|
|
978
|
+
product_sku: 'test-sku'
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
|
|
919
982
|
// Send request with any data (should be ignored)
|
|
920
983
|
const request = createMockRequest({
|
|
921
984
|
path: '/no-params-tool',
|
|
922
985
|
bodyJSON: {
|
|
923
986
|
parameters: {
|
|
924
987
|
unexpected: 'value'
|
|
925
|
-
}
|
|
988
|
+
},
|
|
989
|
+
auth: authData
|
|
926
990
|
}
|
|
927
991
|
});
|
|
928
992
|
|
|
@@ -932,7 +996,7 @@ describe('ToolsService', () => {
|
|
|
932
996
|
expect(toolWithoutParams.handler).toHaveBeenCalledWith(
|
|
933
997
|
mockToolFunction,
|
|
934
998
|
{ unexpected: 'value' },
|
|
935
|
-
|
|
999
|
+
authData
|
|
936
1000
|
);
|
|
937
1001
|
});
|
|
938
1002
|
});
|