@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.
Files changed (44) hide show
  1. package/README.md +51 -13
  2. package/dist/auth/AuthUtils.d.ts +2 -5
  3. package/dist/auth/AuthUtils.d.ts.map +1 -1
  4. package/dist/auth/AuthUtils.js +5 -5
  5. package/dist/auth/AuthUtils.js.map +1 -1
  6. package/dist/decorator/Decorator.test.js +4 -4
  7. package/dist/decorator/Decorator.test.js.map +1 -1
  8. package/dist/function/GlobalToolFunction.d.ts +4 -1
  9. package/dist/function/GlobalToolFunction.d.ts.map +1 -1
  10. package/dist/function/GlobalToolFunction.js +27 -21
  11. package/dist/function/GlobalToolFunction.js.map +1 -1
  12. package/dist/function/GlobalToolFunction.test.js +114 -193
  13. package/dist/function/GlobalToolFunction.test.js.map +1 -1
  14. package/dist/function/ToolFunction.d.ts +4 -1
  15. package/dist/function/ToolFunction.d.ts.map +1 -1
  16. package/dist/function/ToolFunction.js +20 -21
  17. package/dist/function/ToolFunction.js.map +1 -1
  18. package/dist/function/ToolFunction.test.js +73 -263
  19. package/dist/function/ToolFunction.test.js.map +1 -1
  20. package/dist/service/Service.d.ts +20 -19
  21. package/dist/service/Service.d.ts.map +1 -1
  22. package/dist/service/Service.js +47 -72
  23. package/dist/service/Service.js.map +1 -1
  24. package/dist/service/Service.test.js +229 -133
  25. package/dist/service/Service.test.js.map +1 -1
  26. package/dist/types/Models.d.ts +18 -7
  27. package/dist/types/Models.d.ts.map +1 -1
  28. package/dist/types/Models.js +1 -29
  29. package/dist/types/Models.js.map +1 -1
  30. package/dist/utils/ErrorFormatter.d.ts +9 -0
  31. package/dist/utils/ErrorFormatter.d.ts.map +1 -0
  32. package/dist/utils/ErrorFormatter.js +25 -0
  33. package/dist/utils/ErrorFormatter.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/auth/AuthUtils.ts +10 -10
  36. package/src/decorator/Decorator.test.ts +4 -4
  37. package/src/function/GlobalToolFunction.test.ts +113 -213
  38. package/src/function/GlobalToolFunction.ts +31 -31
  39. package/src/function/ToolFunction.test.ts +78 -285
  40. package/src/function/ToolFunction.ts +24 -30
  41. package/src/service/Service.test.ts +238 -174
  42. package/src/service/Service.ts +68 -92
  43. package/src/types/Models.ts +24 -15
  44. 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, OptiIdAuthDataCredentials, OptiIdAuthData } from '../types/Models';
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<unknown>;
81
- let mockInteraction: Interaction<unknown>;
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
- undefined
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
- undefined
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
- undefined
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 = new OptiIdAuthData(
370
- 'optiId',
371
- new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
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
- undefined
445
+ { param1: 'test-value', auth: authData },
446
+ authData
408
447
  );
409
448
  });
410
449
 
411
- it('should return 500 error in RFC 9457 format when tool handler throws a regular error', async () => {
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(response.status).toBe(500);
419
- expect(response.bodyJSON).toEqual({
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 return 500 error with generic message when error has no message', async () => {
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(response.status).toBe(500);
439
- expect(response.bodyJSON).toEqual({
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 return custom status code when tool handler throws ToolError', async () => {
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(response.status).toBe(404);
456
- expect(response.bodyJSON).toEqual({
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 return ToolError without detail field when detail is not provided', async () => {
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(response.status).toBe(400);
477
- expect(response.bodyJSON).toEqual({
478
- title: 'Bad request',
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 to 500 when ToolError is created without status', async () => {
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(response.status).toBe(500);
494
- expect(response.bodyJSON).toEqual({
495
- title: 'Database error',
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' }, undefined);
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).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
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 = new OptiIdAuthData(
540
- 'optiId',
541
- new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
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 = new OptiIdAuthData(
568
- 'optiId',
569
- new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
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 return 500 error in RFC 9457 format when interaction handler throws a regular error', async () => {
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
- const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
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 return custom status code when interaction handler throws ToolError', async () => {
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
- const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
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 return 404 when no matching tool or interaction is found', async () => {
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(response.status).toBe(404);
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 handle request with null bodyJSON', async () => {
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
- const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
738
+ await expect(toolsService.processRequest(requestWithNullBody, mockToolFunction))
739
+ .rejects.toThrow(ToolError);
706
740
 
707
- expect(response.status).toBe(200);
708
- expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
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 handle request with undefined bodyJSON', async () => {
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
- const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
773
+ await expect(toolsService.processRequest(requestWithUndefinedBody, mockToolFunction))
774
+ .rejects.toThrow(ToolError);
736
775
 
737
- expect(response.status).toBe(200);
738
- expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
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 = new OptiIdAuthData(
751
- 'optiId',
752
- new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
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 handle missing auth data gracefully', async () => {
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
- const response = await toolsService.processRequest(requestWithoutAuth, mockToolFunction);
843
+ await expect(toolsService.processRequest(requestWithoutAuth, mockToolFunction))
844
+ .rejects.toThrow(ToolError);
796
845
 
797
- expect(response.status).toBe(200);
798
- expect(mockTool.handler).toHaveBeenCalledWith(
799
- mockToolFunction, // functionContext
800
- { param1: 'test-value' },
801
- undefined
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 = new OptiIdAuthData(
815
- 'optiId',
816
- new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
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 validate parameters and return 400 for invalid types', async () => {
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
- const response = await toolsService.processRequest(invalidRequest, mockToolFunction);
878
-
879
- expect(response.status).toBe(400);
880
-
881
- // Expect RFC 9457 Problem Details format
882
- expect(response.bodyJSON).toHaveProperty('title', 'One or more validation errors occurred.');
883
- expect(response.bodyJSON).toHaveProperty('status', 400);
884
- expect(response.bodyJSON).toHaveProperty('detail', 'See \'errors\' field for details.');
885
- expect(response.bodyJSON).toHaveProperty('instance', '/typed-tool');
886
- expect(response.bodyJSON).toHaveProperty('errors');
887
- expect(response.bodyJSON.errors).toHaveLength(3);
888
-
889
- // Check error structure - field and message
890
- const errors = response.bodyJSON.errors;
891
- expect(errors[0]).toHaveProperty('field', 'name');
892
- expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
893
-
894
- // Check that the content type is set to application/problem+json for RFC 9457 compliance
895
- expect(response.headers).toBeDefined();
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
- undefined
999
+ authData
936
1000
  );
937
1001
  });
938
1002
  });