@optimizely-opal/opal-tool-ocp-sdk 0.0.0-OCP-1487.1

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 (72) hide show
  1. package/README.md +631 -0
  2. package/dist/auth/AuthUtils.d.ts +31 -0
  3. package/dist/auth/AuthUtils.d.ts.map +1 -0
  4. package/dist/auth/AuthUtils.js +64 -0
  5. package/dist/auth/AuthUtils.js.map +1 -0
  6. package/dist/auth/AuthUtils.test.d.ts +2 -0
  7. package/dist/auth/AuthUtils.test.d.ts.map +1 -0
  8. package/dist/auth/AuthUtils.test.js +469 -0
  9. package/dist/auth/AuthUtils.test.js.map +1 -0
  10. package/dist/auth/TokenVerifier.d.ts +31 -0
  11. package/dist/auth/TokenVerifier.d.ts.map +1 -0
  12. package/dist/auth/TokenVerifier.js +127 -0
  13. package/dist/auth/TokenVerifier.js.map +1 -0
  14. package/dist/auth/TokenVerifier.test.d.ts +2 -0
  15. package/dist/auth/TokenVerifier.test.d.ts.map +1 -0
  16. package/dist/auth/TokenVerifier.test.js +125 -0
  17. package/dist/auth/TokenVerifier.test.js.map +1 -0
  18. package/dist/decorator/Decorator.d.ts +48 -0
  19. package/dist/decorator/Decorator.d.ts.map +1 -0
  20. package/dist/decorator/Decorator.js +53 -0
  21. package/dist/decorator/Decorator.js.map +1 -0
  22. package/dist/decorator/Decorator.test.d.ts +2 -0
  23. package/dist/decorator/Decorator.test.d.ts.map +1 -0
  24. package/dist/decorator/Decorator.test.js +528 -0
  25. package/dist/decorator/Decorator.test.js.map +1 -0
  26. package/dist/function/GlobalToolFunction.d.ts +28 -0
  27. package/dist/function/GlobalToolFunction.d.ts.map +1 -0
  28. package/dist/function/GlobalToolFunction.js +56 -0
  29. package/dist/function/GlobalToolFunction.js.map +1 -0
  30. package/dist/function/GlobalToolFunction.test.d.ts +2 -0
  31. package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
  32. package/dist/function/GlobalToolFunction.test.js +425 -0
  33. package/dist/function/GlobalToolFunction.test.js.map +1 -0
  34. package/dist/function/ToolFunction.d.ts +28 -0
  35. package/dist/function/ToolFunction.d.ts.map +1 -0
  36. package/dist/function/ToolFunction.js +60 -0
  37. package/dist/function/ToolFunction.js.map +1 -0
  38. package/dist/function/ToolFunction.test.d.ts +2 -0
  39. package/dist/function/ToolFunction.test.d.ts.map +1 -0
  40. package/dist/function/ToolFunction.test.js +314 -0
  41. package/dist/function/ToolFunction.test.js.map +1 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +26 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/service/Service.d.ts +80 -0
  47. package/dist/service/Service.d.ts.map +1 -0
  48. package/dist/service/Service.js +210 -0
  49. package/dist/service/Service.js.map +1 -0
  50. package/dist/service/Service.test.d.ts +2 -0
  51. package/dist/service/Service.test.d.ts.map +1 -0
  52. package/dist/service/Service.test.js +427 -0
  53. package/dist/service/Service.test.js.map +1 -0
  54. package/dist/types/Models.d.ts +126 -0
  55. package/dist/types/Models.d.ts.map +1 -0
  56. package/dist/types/Models.js +181 -0
  57. package/dist/types/Models.js.map +1 -0
  58. package/package.json +64 -0
  59. package/src/auth/AuthUtils.test.ts +586 -0
  60. package/src/auth/AuthUtils.ts +66 -0
  61. package/src/auth/TokenVerifier.test.ts +165 -0
  62. package/src/auth/TokenVerifier.ts +145 -0
  63. package/src/decorator/Decorator.test.ts +649 -0
  64. package/src/decorator/Decorator.ts +111 -0
  65. package/src/function/GlobalToolFunction.test.ts +505 -0
  66. package/src/function/GlobalToolFunction.ts +61 -0
  67. package/src/function/ToolFunction.test.ts +374 -0
  68. package/src/function/ToolFunction.ts +64 -0
  69. package/src/index.ts +5 -0
  70. package/src/service/Service.test.ts +661 -0
  71. package/src/service/Service.ts +213 -0
  72. package/src/types/Models.ts +163 -0
@@ -0,0 +1,661 @@
1
+ import { toolsService, Tool, Interaction } from './Service';
2
+ import { Parameter, ParameterType, AuthRequirement, OptiIdAuthDataCredentials, OptiIdAuthData } from '../types/Models';
3
+ import { ToolFunction } from '../function/ToolFunction';
4
+ import { logger } from '@zaiusinc/app-sdk';
5
+
6
+ // Mock the logger and other app-sdk exports
7
+ jest.mock('@zaiusinc/app-sdk', () => ({
8
+ logger: {
9
+ error: jest.fn()
10
+ },
11
+ Function: class {
12
+ public constructor(public request: any) {}
13
+ },
14
+ Response: jest.fn().mockImplementation((status, data) => ({
15
+ status,
16
+ data,
17
+ bodyJSON: data,
18
+ bodyAsU8Array: new Uint8Array()
19
+ }))
20
+ }));
21
+
22
+ describe('ToolsService', () => {
23
+ let mockTool: Tool<unknown>;
24
+ let mockInteraction: Interaction<unknown>;
25
+ let mockToolFunction: ToolFunction;
26
+
27
+ beforeEach(() => {
28
+ // Clear registered functions and interactions before each test
29
+ (toolsService as any).functions = new Map();
30
+ (toolsService as any).interactions = new Map();
31
+
32
+ // Reset all mocks
33
+ jest.clearAllMocks();
34
+
35
+ // Create mock ToolFunction
36
+ mockToolFunction = {
37
+ ready: jest.fn().mockResolvedValue(true),
38
+ perform: jest.fn(),
39
+ validateBearerToken: jest.fn().mockReturnValue(true),
40
+ request: {} as any
41
+ } as any;
42
+
43
+ // Create mock tool handler
44
+ const mockToolHandler = jest.fn().mockResolvedValue({ result: 'success' });
45
+
46
+ // Create mock interaction handler
47
+ const mockInteractionHandler = jest.fn().mockResolvedValue({ message: 'interaction success' });
48
+
49
+ // Create mock tool
50
+ mockTool = new Tool(
51
+ 'testTool',
52
+ 'Test tool description',
53
+ [new Parameter('param1', ParameterType.String, 'Test parameter', true)],
54
+ '/test-tool',
55
+ mockToolHandler
56
+ );
57
+
58
+ // Create mock interaction
59
+ mockInteraction = new Interaction(
60
+ 'testInteraction',
61
+ '/test-interaction',
62
+ mockInteractionHandler
63
+ );
64
+ });
65
+
66
+ const createMockRequest = (overrides: any = {}): any => {
67
+ // Create a mock headers object with get method
68
+ const createHeadersMap = (headersObj: any = {}) => {
69
+ const map = new Map();
70
+ Object.entries(headersObj).forEach(([key, value]) => {
71
+ map.set(key, value);
72
+ });
73
+ return map;
74
+ };
75
+
76
+ const baseRequest = {
77
+ path: '/test-tool',
78
+ method: 'POST',
79
+ bodyJSON: { parameters: { param1: 'test-value' } },
80
+ body: JSON.stringify({ parameters: { param1: 'test-value' } }),
81
+ bodyData: Buffer.from(''),
82
+ headers: createHeadersMap(),
83
+ params: {},
84
+ contentType: 'application/json'
85
+ };
86
+
87
+ return {
88
+ ...baseRequest,
89
+ ...overrides,
90
+ headers: createHeadersMap(overrides.headers)
91
+ };
92
+ };
93
+
94
+ describe('processRequest', () => {
95
+ describe('discovery endpoint', () => {
96
+ it('should return registered functions for discovery endpoint', async () => {
97
+ toolsService.registerTool(
98
+ mockTool.name,
99
+ mockTool.description,
100
+ mockTool.handler,
101
+ mockTool.parameters,
102
+ mockTool.endpoint
103
+ );
104
+
105
+ const discoveryRequest = createMockRequest({ path: '/discovery' });
106
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
107
+
108
+ expect(response.status).toBe(200);
109
+ expect(response).toHaveProperty('bodyJSON');
110
+ expect(response.bodyAsU8Array).toBeDefined();
111
+
112
+ // Test the actual response structure
113
+ const responseData = response.bodyJSON as { functions: unknown[] };
114
+ expect(responseData).toHaveProperty('functions');
115
+ expect(Array.isArray(responseData.functions)).toBe(true);
116
+ expect(responseData.functions).toHaveLength(1);
117
+
118
+ // Test the registered function structure
119
+ const registeredFunction = responseData.functions[0];
120
+ expect(registeredFunction).toEqual({
121
+ name: mockTool.name,
122
+ description: mockTool.description,
123
+ parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
124
+ endpoint: mockTool.endpoint,
125
+ http_method: 'POST',
126
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
127
+ });
128
+ });
129
+
130
+ it('should return empty functions array when no tools are registered', async () => {
131
+ const discoveryRequest = createMockRequest({ path: '/discovery' });
132
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
133
+
134
+ expect(response.status).toBe(200);
135
+ expect(response.bodyAsU8Array).toBeDefined();
136
+
137
+ // Test the actual response structure
138
+ const responseData = response.bodyJSON as { functions: unknown[] };
139
+ expect(responseData).toHaveProperty('functions');
140
+ expect(Array.isArray(responseData.functions)).toBe(true);
141
+ expect(responseData.functions).toHaveLength(0);
142
+ });
143
+
144
+ it('should return multiple registered functions in discovery endpoint', async () => {
145
+ // Register first tool
146
+ toolsService.registerTool(
147
+ mockTool.name,
148
+ mockTool.description,
149
+ mockTool.handler,
150
+ mockTool.parameters,
151
+ mockTool.endpoint
152
+ );
153
+
154
+ // Register second tool with auth requirements
155
+ const authRequirements = [new AuthRequirement('oauth2', 'calendar', true)];
156
+ toolsService.registerTool(
157
+ 'second_tool',
158
+ 'Second test tool',
159
+ jest.fn(),
160
+ [],
161
+ '/second-tool',
162
+ authRequirements
163
+ );
164
+
165
+ const discoveryRequest = createMockRequest({ path: '/discovery' });
166
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
167
+
168
+ expect(response.status).toBe(200);
169
+
170
+ // Test the actual response structure
171
+ const responseData = response.bodyJSON as { functions: unknown[] };
172
+ expect(responseData).toHaveProperty('functions');
173
+ expect(Array.isArray(responseData.functions)).toBe(true);
174
+ expect(responseData.functions).toHaveLength(2);
175
+
176
+ // Find and test both functions
177
+ const firstFunction = responseData.functions.find((f: any) => f.name === mockTool.name);
178
+ const secondFunction = responseData.functions.find((f: any) => f.name === 'second_tool');
179
+
180
+ expect(firstFunction).toEqual({
181
+ name: mockTool.name,
182
+ description: mockTool.description,
183
+ parameters: mockTool.parameters.map((p: Parameter) => p.toJSON()),
184
+ endpoint: mockTool.endpoint,
185
+ http_method: 'POST',
186
+ auth_requirements: [{ provider: 'OptiID', scope_bundle: 'default', required: true }]
187
+ });
188
+
189
+ expect(secondFunction).toEqual({
190
+ name: 'second_tool',
191
+ description: 'Second test tool',
192
+ parameters: [],
193
+ endpoint: '/second-tool',
194
+ http_method: 'POST',
195
+ auth_requirements: [
196
+ { provider: 'oauth2', scope_bundle: 'calendar', required: true },
197
+ { provider: 'OptiID', scope_bundle: 'default', required: true }
198
+ ]
199
+ });
200
+ });
201
+ });
202
+
203
+ describe('tool execution', () => {
204
+ beforeEach(() => {
205
+ toolsService.registerTool(
206
+ mockTool.name,
207
+ mockTool.description,
208
+ mockTool.handler,
209
+ mockTool.parameters,
210
+ mockTool.endpoint
211
+ );
212
+ });
213
+
214
+ it('should execute tool successfully with parameters', async () => {
215
+ const mockRequest = createMockRequest();
216
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
217
+
218
+ expect(response.status).toBe(200);
219
+ expect(mockTool.handler).toHaveBeenCalledWith(
220
+ mockToolFunction, // functionContext
221
+ { param1: 'test-value' },
222
+ undefined
223
+ );
224
+ });
225
+
226
+ it('should execute tool with existing ToolFunction instance context', async () => {
227
+ // Create a mock ToolFunction instance
228
+ const mockToolFunctionInstance = {
229
+ someProperty: 'test-value',
230
+ someMethod: jest.fn(),
231
+ ready: jest.fn().mockResolvedValue(true),
232
+ perform: jest.fn(),
233
+ validateBearerToken: jest.fn().mockReturnValue(true),
234
+ request: {} as any
235
+ } as any;
236
+
237
+ const mockRequest = createMockRequest();
238
+ const response = await toolsService.processRequest(mockRequest, mockToolFunctionInstance);
239
+
240
+ expect(response.status).toBe(200);
241
+ expect(mockTool.handler).toHaveBeenCalledWith(
242
+ mockToolFunctionInstance, // functionContext - existing instance
243
+ { param1: 'test-value' },
244
+ undefined
245
+ );
246
+ });
247
+
248
+ it('should allow handler in ToolFunction subclass to access request object', async () => {
249
+ // Create a mock class that extends ToolFunction
250
+ class MockToolFunction extends ToolFunction {
251
+ public testMethod() {
252
+ return `path: ${this.request.path}`;
253
+ }
254
+
255
+ public getRequestPath() {
256
+ return this.request.path;
257
+ }
258
+ }
259
+
260
+ // Create an instance with a mock request
261
+ const mockRequest = createMockRequest({ path: '/test-path' });
262
+ const mockToolFunctionInstance = new MockToolFunction(mockRequest);
263
+
264
+ // Create a handler that will use the ToolFunction instance's methods and properties
265
+ const handlerThatAccessesRequest = jest.fn().mockImplementation((
266
+ functionContext: any,
267
+ params: any,
268
+ _authData: any
269
+ ) => {
270
+ // This simulates what would happen in a decorated method of a ToolFunction subclass
271
+ if (functionContext && functionContext instanceof MockToolFunction) {
272
+ return Promise.resolve({
273
+ success: true,
274
+ requestPath: functionContext.getRequestPath(), // Use public method to access request
275
+ testMethodResult: functionContext.testMethod(),
276
+ receivedParams: params
277
+ });
278
+ }
279
+ return Promise.resolve({ success: false, error: 'No valid function context' });
280
+ });
281
+
282
+ // Register a tool with our custom handler
283
+ toolsService.registerTool(
284
+ 'test-toolfunction-access',
285
+ 'Test handler access to ToolFunction instance',
286
+ handlerThatAccessesRequest,
287
+ [],
288
+ '/test-toolfunction-access'
289
+ );
290
+
291
+ const testRequest = createMockRequest({
292
+ path: '/test-toolfunction-access',
293
+ bodyJSON: { action: 'test' }
294
+ });
295
+
296
+ const response = await toolsService.processRequest(testRequest, mockToolFunctionInstance);
297
+
298
+ expect(response.status).toBe(200);
299
+ expect((response as any).data).toBeDefined();
300
+ expect((response as any).data.success).toBe(true);
301
+ expect((response as any).data.requestPath).toBe('/test-path');
302
+ expect((response as any).data.testMethodResult).toBe('path: /test-path');
303
+ expect((response as any).data.receivedParams).toEqual({ action: 'test' });
304
+ expect(handlerThatAccessesRequest).toHaveBeenCalledWith(
305
+ mockToolFunctionInstance, // functionContext is the ToolFunction instance
306
+ { action: 'test' },
307
+ undefined
308
+ );
309
+ });
310
+
311
+ it('should execute tool with OptiID auth data when provided', async () => {
312
+ const authData = new OptiIdAuthData(
313
+ 'optiId',
314
+ new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
315
+ );
316
+
317
+ const requestWithAuth = createMockRequest({
318
+ bodyJSON: {
319
+ parameters: { param1: 'test-value' },
320
+ auth: authData
321
+ },
322
+ body: JSON.stringify({
323
+ parameters: { param1: 'test-value' },
324
+ auth: authData
325
+ })
326
+ });
327
+
328
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
329
+
330
+ expect(response.status).toBe(200);
331
+ expect(mockTool.handler).toHaveBeenCalledWith(
332
+ mockToolFunction, // functionContext
333
+ { param1: 'test-value' },
334
+ authData
335
+ );
336
+ });
337
+
338
+ it('should handle request body without parameters wrapper', async () => {
339
+ const requestWithoutWrapper = createMockRequest({
340
+ bodyJSON: { param1: 'test-value' },
341
+ body: JSON.stringify({ param1: 'test-value' })
342
+ });
343
+
344
+ const response = await toolsService.processRequest(requestWithoutWrapper, mockToolFunction);
345
+
346
+ expect(response.status).toBe(200);
347
+ expect(mockTool.handler).toHaveBeenCalledWith(
348
+ mockToolFunction, // functionContext
349
+ { param1: 'test-value' },
350
+ undefined
351
+ );
352
+ });
353
+
354
+ it('should return 500 error when tool handler throws an error', async () => {
355
+ const errorMessage = 'Tool execution failed';
356
+ jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
357
+
358
+ const mockRequest = createMockRequest();
359
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
360
+
361
+ expect(response.status).toBe(500);
362
+ expect(logger.error).toHaveBeenCalledWith(
363
+ `Error in function ${mockTool.name}:`,
364
+ expect.any(Error)
365
+ );
366
+ });
367
+
368
+ it('should return 500 error with generic message when error has no message', async () => {
369
+ jest.mocked(mockTool.handler).mockRejectedValueOnce({});
370
+
371
+ const mockRequest = createMockRequest();
372
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
373
+
374
+ expect(response.status).toBe(500);
375
+ });
376
+ });
377
+
378
+ describe('interaction execution', () => {
379
+ beforeEach(() => {
380
+ toolsService.registerInteraction(
381
+ mockInteraction.name,
382
+ mockInteraction.handler,
383
+ mockInteraction.endpoint
384
+ );
385
+ });
386
+
387
+ it('should execute interaction successfully with data', async () => {
388
+ const interactionRequest = createMockRequest({
389
+ path: '/test-interaction',
390
+ bodyJSON: { data: { param1: 'test-value' } },
391
+ body: JSON.stringify({ data: { param1: 'test-value' } })
392
+ });
393
+
394
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
395
+
396
+ expect(response.status).toBe(200);
397
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
398
+ });
399
+
400
+ it('should handle interaction request body without data wrapper', async () => {
401
+ const interactionRequest = createMockRequest({
402
+ path: '/test-interaction',
403
+ bodyJSON: { param1: 'test-value' },
404
+ body: JSON.stringify({ param1: 'test-value' })
405
+ });
406
+
407
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
408
+
409
+ expect(response.status).toBe(200);
410
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
411
+ });
412
+
413
+ it('should execute interaction with OptiID auth data when provided', async () => {
414
+ const authData = new OptiIdAuthData(
415
+ 'optiId',
416
+ new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
417
+ );
418
+
419
+ const interactionRequest = createMockRequest({
420
+ path: '/test-interaction',
421
+ bodyJSON: {
422
+ data: { param1: 'test-value' },
423
+ auth: authData
424
+ },
425
+ body: JSON.stringify({
426
+ data: { param1: 'test-value' },
427
+ auth: authData
428
+ })
429
+ });
430
+
431
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
432
+
433
+ expect(response.status).toBe(200);
434
+ expect(mockInteraction.handler).toHaveBeenCalledWith(
435
+ mockToolFunction, // functionContext
436
+ { param1: 'test-value' },
437
+ authData
438
+ );
439
+ });
440
+
441
+ it('should handle interaction request without data wrapper but with auth data', async () => {
442
+ const authData = new OptiIdAuthData(
443
+ 'optiId',
444
+ new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
445
+ );
446
+
447
+ const interactionRequest = createMockRequest({
448
+ path: '/test-interaction',
449
+ bodyJSON: {
450
+ param1: 'test-value',
451
+ auth: authData
452
+ },
453
+ body: JSON.stringify({
454
+ param1: 'test-value',
455
+ auth: authData
456
+ })
457
+ });
458
+
459
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
460
+
461
+ expect(response.status).toBe(200);
462
+ expect(mockInteraction.handler).toHaveBeenCalledWith(
463
+ mockToolFunction, // functionContext
464
+ {
465
+ param1: 'test-value',
466
+ auth: authData
467
+ },
468
+ authData
469
+ );
470
+ });
471
+
472
+ it('should return 500 error when interaction handler throws an error', async () => {
473
+ const errorMessage = 'Interaction execution failed';
474
+ jest.mocked(mockInteraction.handler).mockRejectedValueOnce(new Error(errorMessage));
475
+
476
+ const interactionRequest = createMockRequest({
477
+ path: '/test-interaction',
478
+ bodyJSON: { data: { param1: 'test-value' } }
479
+ });
480
+
481
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
482
+
483
+ expect(response.status).toBe(500);
484
+ expect(logger.error).toHaveBeenCalledWith(
485
+ `Error in function ${mockInteraction.name}:`,
486
+ expect.any(Error)
487
+ );
488
+ });
489
+ });
490
+
491
+ describe('error cases', () => {
492
+ it('should return 404 when no matching tool or interaction is found', async () => {
493
+ const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
494
+ const response = await toolsService.processRequest(unknownRequest, mockToolFunction);
495
+
496
+ expect(response.status).toBe(404);
497
+ });
498
+
499
+ it('should handle tool with OptiID auth requirements', async () => {
500
+ const authRequirements = [
501
+ new AuthRequirement('OptiID', 'calendar', true)
502
+ ];
503
+
504
+ toolsService.registerTool(
505
+ 'optiIdAuthTool',
506
+ 'Tool with OptiID auth',
507
+ mockTool.handler,
508
+ mockTool.parameters,
509
+ '/optid-auth-tool',
510
+ authRequirements
511
+ );
512
+
513
+ const authRequest = createMockRequest({
514
+ path: '/optid-auth-tool'
515
+ });
516
+
517
+ const response = await toolsService.processRequest(authRequest, mockToolFunction);
518
+
519
+ expect(response.status).toBe(200);
520
+ });
521
+ });
522
+
523
+ describe('edge cases', () => {
524
+ it('should handle request with null bodyJSON', async () => {
525
+ toolsService.registerTool(
526
+ mockTool.name,
527
+ mockTool.description,
528
+ mockTool.handler,
529
+ mockTool.parameters,
530
+ mockTool.endpoint
531
+ );
532
+
533
+ const requestWithNullBody = createMockRequest({
534
+ bodyJSON: null,
535
+ body: null
536
+ });
537
+
538
+ const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
539
+
540
+ expect(response.status).toBe(200);
541
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
542
+ });
543
+
544
+ it('should handle request with undefined bodyJSON', async () => {
545
+ toolsService.registerTool(
546
+ mockTool.name,
547
+ mockTool.description,
548
+ mockTool.handler,
549
+ mockTool.parameters,
550
+ mockTool.endpoint
551
+ );
552
+
553
+ const requestWithUndefinedBody = createMockRequest({
554
+ bodyJSON: undefined,
555
+ body: undefined
556
+ });
557
+
558
+ const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
559
+
560
+ expect(response.status).toBe(200);
561
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
562
+ });
563
+
564
+ it('should extract auth data from bodyJSON when body exists', async () => {
565
+ toolsService.registerTool(
566
+ mockTool.name,
567
+ mockTool.description,
568
+ mockTool.handler,
569
+ mockTool.parameters,
570
+ mockTool.endpoint
571
+ );
572
+
573
+ const authData = new OptiIdAuthData(
574
+ 'optiId',
575
+ new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
576
+ );
577
+
578
+ const requestWithAuth = createMockRequest({
579
+ bodyJSON: {
580
+ parameters: { param1: 'test-value' },
581
+ auth: authData
582
+ },
583
+ body: JSON.stringify({
584
+ parameters: { param1: 'test-value' },
585
+ auth: authData
586
+ })
587
+ });
588
+
589
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
590
+
591
+ expect(response.status).toBe(200);
592
+ expect(mockTool.handler).toHaveBeenCalledWith(
593
+ mockToolFunction, // functionContext
594
+ { param1: 'test-value' },
595
+ authData
596
+ );
597
+ });
598
+
599
+ it('should handle missing auth data gracefully', async () => {
600
+ toolsService.registerTool(
601
+ mockTool.name,
602
+ mockTool.description,
603
+ mockTool.handler,
604
+ mockTool.parameters,
605
+ mockTool.endpoint
606
+ );
607
+
608
+ const requestWithoutAuth = createMockRequest({
609
+ bodyJSON: {
610
+ parameters: { param1: 'test-value' }
611
+ // No auth property
612
+ },
613
+ body: JSON.stringify({
614
+ parameters: { param1: 'test-value' }
615
+ })
616
+ });
617
+
618
+ const response = await toolsService.processRequest(requestWithoutAuth, mockToolFunction);
619
+
620
+ expect(response.status).toBe(200);
621
+ expect(mockTool.handler).toHaveBeenCalledWith(
622
+ mockToolFunction, // functionContext
623
+ { param1: 'test-value' },
624
+ undefined
625
+ );
626
+ });
627
+
628
+ it('should handle auth extraction when body is falsy but bodyJSON has auth', async () => {
629
+ toolsService.registerTool(
630
+ mockTool.name,
631
+ mockTool.description,
632
+ mockTool.handler,
633
+ mockTool.parameters,
634
+ mockTool.endpoint
635
+ );
636
+
637
+ const authData = new OptiIdAuthData(
638
+ 'optiId',
639
+ new OptiIdAuthDataCredentials('customer123', 'instance123', 'token123', 'sku123')
640
+ );
641
+
642
+ const requestWithAuthButNoBody = createMockRequest({
643
+ bodyJSON: {
644
+ parameters: { param1: 'test-value' },
645
+ auth: authData
646
+ },
647
+ body: ''
648
+ });
649
+
650
+ const response = await toolsService.processRequest(requestWithAuthButNoBody, mockToolFunction);
651
+
652
+ expect(response.status).toBe(200);
653
+ expect(mockTool.handler).toHaveBeenCalledWith(
654
+ mockToolFunction, // functionContext
655
+ { param1: 'test-value' },
656
+ authData
657
+ );
658
+ });
659
+ });
660
+ });
661
+ });