@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
@@ -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' }, undefined);
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' }, undefined);
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' }, undefined);
291
+ { action: 'test', auth: authData }, authData);
271
292
  });
272
293
  it('should execute tool with OptiID auth data when provided', async () => {
273
- const authData = new Models_1.OptiIdAuthData('optiId', new Models_1.OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123'));
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' }, undefined);
335
+ { param1: 'test-value', auth: authData }, authData);
298
336
  });
299
- it('should return 500 error in RFC 9457 format when tool handler throws a regular error', async () => {
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
- const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
304
- expect(response.status).toBe(500);
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 return 500 error with generic message when error has no message', async () => {
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
- const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
318
- expect(response.status).toBe(500);
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 return custom status code when tool handler throws ToolError', async () => {
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
- const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
332
- expect(response.status).toBe(404);
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 return ToolError without detail field when detail is not provided', async () => {
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
- const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
347
- expect(response.status).toBe(400);
348
- expect(response.bodyJSON).toEqual({
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 to 500 when ToolError is created without status', async () => {
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
- const response = await Service_1.toolsService.processRequest(mockRequest, mockToolFunction);
361
- expect(response.status).toBe(500);
362
- expect(response.bodyJSON).toEqual({
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' }, undefined);
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).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
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 = new Models_1.OptiIdAuthData('optiId', new Models_1.OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123'));
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 = new Models_1.OptiIdAuthData('optiId', new Models_1.OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123'));
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 return 500 error in RFC 9457 format when interaction handler throws a regular error', async () => {
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
- const response = await Service_1.toolsService.processRequest(interactionRequest, mockToolFunction);
441
- expect(response.status).toBe(500);
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 return custom status code when interaction handler throws ToolError', async () => {
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(response.headers.get('content-type')).toBe('application/problem+json');
467
- expect(app_sdk_1.logger.error).toHaveBeenCalledWith(`Error in function ${mockInteraction.name}:`, expect.any(ToolError_1.ToolError));
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 return 404 when no matching tool or interaction is found', async () => {
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
- const response = await Service_1.toolsService.processRequest(unknownRequest, mockToolFunction);
474
- expect(response.status).toBe(404);
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 handle request with null bodyJSON', async () => {
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
- const response = await Service_1.toolsService.processRequest(requestWithNullBody, mockToolFunction);
505
- expect(response.status).toBe(200);
506
- expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
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 handle request with undefined bodyJSON', async () => {
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
- const response = await Service_1.toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
524
- expect(response.status).toBe(200);
525
- expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
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 = new Models_1.OptiIdAuthData('optiId', new Models_1.OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123'));
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 handle missing auth data gracefully', async () => {
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
- const response = await Service_1.toolsService.processRequest(requestWithoutAuth, mockToolFunction);
557
- expect(response.status).toBe(200);
558
- expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, // functionContext
559
- { param1: 'test-value' }, undefined);
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 = new Models_1.OptiIdAuthData('optiId', new Models_1.OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123'));
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 validate parameters and return 400 for invalid types', async () => {
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
- const response = await Service_1.toolsService.processRequest(invalidRequest, mockToolFunction);
607
- expect(response.status).toBe(400);
608
- // Expect RFC 9457 Problem Details format
609
- expect(response.bodyJSON).toHaveProperty('title', 'One or more validation errors occurred.');
610
- expect(response.bodyJSON).toHaveProperty('status', 400);
611
- expect(response.bodyJSON).toHaveProperty('detail', 'See \'errors\' field for details.');
612
- expect(response.bodyJSON).toHaveProperty('instance', '/typed-tool');
613
- expect(response.bodyJSON).toHaveProperty('errors');
614
- expect(response.bodyJSON.errors).toHaveLength(3);
615
- // Check error structure - field and message
616
- const errors = response.bodyJSON.errors;
617
- expect(errors[0]).toHaveProperty('field', 'name');
618
- expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
619
- // Check that the content type is set to application/problem+json for RFC 9457 compliance
620
- expect(response.headers).toBeDefined();
621
- expect(response.headers.get('content-type')).toBe('application/problem+json');
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' }, undefined);
741
+ expect(toolWithoutParams.handler).toHaveBeenCalledWith(mockToolFunction, { unexpected: 'value' }, authData);
646
742
  });
647
743
  });
648
744
  });