@optimizely-opal/opal-tool-ocp-sdk 0.0.0-beta.1 → 0.0.0-beta.11

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.
@@ -26,26 +26,33 @@ jest.mock('@zaiusinc/app-sdk', () => ({
26
26
  bodyJSON: data,
27
27
  bodyAsU8Array: new Uint8Array()
28
28
  })),
29
+ amendLogContext: jest.fn(),
29
30
  }));
30
31
 
31
32
  // Create a concrete implementation for testing
32
33
  class TestToolFunction extends ToolFunction {
33
34
  private mockValidateBearerToken: jest.MockedFunction<(token: string) => boolean>;
35
+ private mockReady: jest.MockedFunction<() => Promise<boolean>>;
34
36
 
35
37
  public constructor(request?: any) {
36
38
  super(request || {}); // Pass the request parameter properly
37
39
  // Set the request directly without defaulting to empty object
38
40
  (this as any).request = request;
39
41
 
40
- // Create a mock implementation of the abstract method
41
42
  this.mockValidateBearerToken = jest.fn().mockReturnValue(true);
43
+ this.mockReady = jest.fn().mockResolvedValue(true);
42
44
  }
43
45
 
44
- // Implement the abstract method
46
+ // Override the concrete method with mock implementation for testing
45
47
  protected validateBearerToken(bearerToken: string): boolean {
46
48
  return this.mockValidateBearerToken(bearerToken);
47
49
  }
48
50
 
51
+ // Override the ready method with mock implementation for testing
52
+ protected ready(): Promise<boolean> {
53
+ return this.mockReady();
54
+ }
55
+
49
56
  // Expose request and validation mock for testing
50
57
  public getRequest() {
51
58
  return (this as any).request;
@@ -54,6 +61,10 @@ class TestToolFunction extends ToolFunction {
54
61
  public getMockValidateBearerToken() {
55
62
  return this.mockValidateBearerToken;
56
63
  }
64
+
65
+ public getMockReady() {
66
+ return this.mockReady;
67
+ }
57
68
  }
58
69
 
59
70
  describe('ToolFunction', () => {
@@ -82,6 +93,137 @@ describe('ToolFunction', () => {
82
93
  toolFunction = new TestToolFunction(mockRequest);
83
94
  });
84
95
 
96
+ describe('/ready endpoint', () => {
97
+ it('should return ready: true when ready method returns true', async () => {
98
+ // Arrange
99
+ const readyRequest = {
100
+ headers: new Map(),
101
+ method: 'GET',
102
+ path: '/ready'
103
+ };
104
+ toolFunction = new TestToolFunction(readyRequest);
105
+ toolFunction.getMockReady().mockResolvedValue(true);
106
+
107
+ // Act
108
+ const result = await toolFunction.perform();
109
+
110
+ // Assert
111
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
112
+ expect(result).toEqual(new Response(200, { ready: true }));
113
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
114
+ });
115
+
116
+ it('should return ready: false when ready method returns false', async () => {
117
+ // Arrange
118
+ const readyRequest = {
119
+ headers: new Map(),
120
+ method: 'GET',
121
+ path: '/ready'
122
+ };
123
+ toolFunction = new TestToolFunction(readyRequest);
124
+ toolFunction.getMockReady().mockResolvedValue(false);
125
+
126
+ // Act
127
+ const result = await toolFunction.perform();
128
+
129
+ // Assert
130
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
131
+ expect(result).toEqual(new Response(200, { ready: false }));
132
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
133
+ });
134
+
135
+ it('should handle ready method throwing an error', async () => {
136
+ // Arrange
137
+ const readyRequest = {
138
+ headers: new Map(),
139
+ method: 'GET',
140
+ path: '/ready'
141
+ };
142
+ toolFunction = new TestToolFunction(readyRequest);
143
+ toolFunction.getMockReady().mockRejectedValue(new Error('Ready check failed'));
144
+
145
+ // Act & Assert
146
+ await expect(toolFunction.perform()).rejects.toThrow('Ready check failed');
147
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
148
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
149
+ });
150
+
151
+ it('should handle /ready endpoint after bearer token validation passes', async () => {
152
+ // Arrange
153
+ const readyRequest = {
154
+ headers: new Map([['authorization', 'Bearer valid-token']]),
155
+ method: 'GET',
156
+ path: '/ready'
157
+ };
158
+ toolFunction = new TestToolFunction(readyRequest);
159
+ toolFunction.getMockReady().mockResolvedValue(true);
160
+ mockExtractBearerToken.mockReturnValue('valid-token');
161
+ toolFunction.getMockValidateBearerToken().mockReturnValue(true); // Make sure auth passes so we reach /ready
162
+
163
+ // Act
164
+ const result = await toolFunction.perform();
165
+
166
+ // Assert
167
+ expect(toolFunction.getMockReady()).toHaveBeenCalledTimes(1);
168
+ expect(result).toEqual(new Response(200, { ready: true }));
169
+ expect(mockExtractBearerToken).toHaveBeenCalledWith(readyRequest.headers); // Auth is checked first
170
+ expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith('valid-token'); // Auth validation happens
171
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
172
+ });
173
+
174
+ it('should return 403 when bearer token validation fails even for /ready endpoint', async () => {
175
+ // Arrange
176
+ const readyRequest = {
177
+ headers: new Map([['authorization', 'Bearer invalid-token']]),
178
+ method: 'GET',
179
+ path: '/ready'
180
+ };
181
+ toolFunction = new TestToolFunction(readyRequest);
182
+ toolFunction.getMockReady().mockResolvedValue(true);
183
+ mockExtractBearerToken.mockReturnValue('invalid-token');
184
+ toolFunction.getMockValidateBearerToken().mockReturnValue(false); // Auth fails
185
+
186
+ // Act
187
+ const result = await toolFunction.perform();
188
+
189
+ // Assert
190
+ expect(result).toEqual(new Response(403, { error: 'Forbidden' }));
191
+ expect(mockExtractBearerToken).toHaveBeenCalledWith(readyRequest.headers);
192
+ expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith('invalid-token');
193
+ expect(toolFunction.getMockReady()).not.toHaveBeenCalled(); // Should not reach ready check
194
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
195
+ });
196
+
197
+ it('should use default ready implementation when not overridden', async () => {
198
+ // Create a class that doesn't override ready method
199
+ class DefaultReadyToolFunction extends ToolFunction {
200
+ public constructor(request?: any) {
201
+ super(request || {});
202
+ (this as any).request = request;
203
+ }
204
+
205
+ public getRequest() {
206
+ return (this as any).request;
207
+ }
208
+ }
209
+
210
+ // Arrange
211
+ const readyRequest = {
212
+ headers: new Map(),
213
+ method: 'GET',
214
+ path: '/ready'
215
+ };
216
+ const defaultToolFunction = new DefaultReadyToolFunction(readyRequest);
217
+
218
+ // Act
219
+ const result = await defaultToolFunction.perform();
220
+
221
+ // Assert - Default implementation should return true
222
+ expect(result).toEqual(new Response(200, { ready: true }));
223
+ expect(mockProcessRequest).not.toHaveBeenCalled(); // Should not call service
224
+ });
225
+ });
226
+
85
227
  describe('bearer token validation', () => {
86
228
  it('should extract bearer token from headers and validate it', async () => {
87
229
  // Arrange
@@ -96,7 +238,7 @@ describe('ToolFunction', () => {
96
238
  // Assert
97
239
  expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
98
240
  expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(bearerToken);
99
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
241
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
100
242
  expect(result).toBe(mockResponse);
101
243
  });
102
244
 
@@ -130,7 +272,7 @@ describe('ToolFunction', () => {
130
272
  // Assert
131
273
  expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
132
274
  expect(toolFunction.getMockValidateBearerToken()).toHaveBeenCalledWith(complexToken);
133
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
275
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
134
276
  expect(result).toBe(mockResponse);
135
277
  });
136
278
 
@@ -190,7 +332,7 @@ describe('ToolFunction', () => {
190
332
  // Assert
191
333
  expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
192
334
  expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
193
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
335
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
194
336
  expect(result).toBe(mockResponse);
195
337
  });
196
338
 
@@ -205,7 +347,35 @@ describe('ToolFunction', () => {
205
347
  // Assert
206
348
  expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
207
349
  expect(toolFunction.getMockValidateBearerToken()).not.toHaveBeenCalled();
208
- expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest);
350
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, toolFunction);
351
+ expect(result).toBe(mockResponse);
352
+ });
353
+
354
+ it('should use default validateBearerToken implementation when not overridden', async () => {
355
+ // Create a class that doesn't override validateBearerToken
356
+ class DefaultToolFunction extends ToolFunction {
357
+ public constructor(request?: any) {
358
+ super(request || {});
359
+ (this as any).request = request;
360
+ }
361
+
362
+ public getRequest() {
363
+ return (this as any).request;
364
+ }
365
+ }
366
+
367
+ // Arrange
368
+ const defaultToolFunction = new DefaultToolFunction(mockRequest);
369
+ const bearerToken = 'any-token';
370
+ mockExtractBearerToken.mockReturnValue(bearerToken);
371
+ mockProcessRequest.mockResolvedValue(mockResponse);
372
+
373
+ // Act
374
+ const result = await defaultToolFunction.perform();
375
+
376
+ // Assert - Default implementation should return true and allow request to proceed
377
+ expect(mockExtractBearerToken).toHaveBeenCalledWith(mockRequest.headers);
378
+ expect(mockProcessRequest).toHaveBeenCalledWith(mockRequest, defaultToolFunction);
209
379
  expect(result).toBe(mockResponse);
210
380
  });
211
381
  });
@@ -1,4 +1,4 @@
1
- import { Function, Response } from '@zaiusinc/app-sdk';
1
+ import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
2
2
  import { toolsService } from '../service/Service';
3
3
 
4
4
  /**
@@ -7,19 +7,46 @@ import { toolsService } from '../service/Service';
7
7
  */
8
8
  export abstract class ToolFunction extends Function {
9
9
 
10
+ /**
11
+ * Override this method to implement any required credentials and/or other configuration
12
+ * exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
13
+ * @async
14
+ * @returns true if the opal function is ready to use
15
+ */
16
+ protected ready(): Promise<boolean> {
17
+ return Promise.resolve(true);
18
+ }
19
+
10
20
  /**
11
21
  * Process the incoming request using the tools service
12
22
  *
13
23
  * @returns Response as the HTTP response
14
24
  */
15
25
  public async perform(): Promise<Response> {
26
+ amendLogContext({ opalThreadId: this.request.headers.get('x-opal-thread-id') || '' });
16
27
  const bearerToken = toolsService.extractBearerToken(this.request.headers);
17
28
  if (bearerToken && !this.validateBearerToken(bearerToken)) {
18
29
  return new Response(403, { error: 'Forbidden' });
19
30
  }
20
31
 
21
- return toolsService.processRequest(this.request);
32
+ if (this.request.path === '/ready') {
33
+ const isReady = await this.ready();
34
+ return new Response(200, { ready: isReady });
35
+ }
36
+ // Pass 'this' as context so decorated methods can use the existing instance
37
+ return toolsService.processRequest(this.request, this);
22
38
  }
23
39
 
24
- protected abstract validateBearerToken(bearerToken: string): boolean;
40
+ /**
41
+ * Validates the bearer token for authorization.
42
+ *
43
+ * This method provides a default implementation that accepts all tokens.
44
+ * Subclasses can override this method to implement custom bearer token validation logic.
45
+ *
46
+ * @param _bearerToken - The bearer token extracted from the Authorization header
47
+ * @returns true if the token is valid and the request should proceed, false to return 403 Forbidden
48
+ */
49
+ protected validateBearerToken(_bearerToken: string): boolean {
50
+ return true;
51
+ }
25
52
  }
@@ -1,12 +1,16 @@
1
1
  import { toolsService, Tool, Interaction } from './Service';
2
2
  import { Parameter, ParameterType, AuthRequirement, OptiIdAuthDataCredentials, OptiIdAuthData } from '../types/Models';
3
+ import { ToolFunction } from '../function/ToolFunction';
3
4
  import { logger } from '@zaiusinc/app-sdk';
4
5
 
5
- // Mock the logger
6
+ // Mock the logger and other app-sdk exports
6
7
  jest.mock('@zaiusinc/app-sdk', () => ({
7
8
  logger: {
8
9
  error: jest.fn()
9
10
  },
11
+ Function: class {
12
+ public constructor(public request: any) {}
13
+ },
10
14
  Response: jest.fn().mockImplementation((status, data) => ({
11
15
  status,
12
16
  data,
@@ -18,6 +22,7 @@ jest.mock('@zaiusinc/app-sdk', () => ({
18
22
  describe('ToolsService', () => {
19
23
  let mockTool: Tool<unknown>;
20
24
  let mockInteraction: Interaction<unknown>;
25
+ let mockToolFunction: ToolFunction;
21
26
 
22
27
  beforeEach(() => {
23
28
  // Clear registered functions and interactions before each test
@@ -27,6 +32,14 @@ describe('ToolsService', () => {
27
32
  // Reset all mocks
28
33
  jest.clearAllMocks();
29
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
+
30
43
  // Create mock tool handler
31
44
  const mockToolHandler = jest.fn().mockResolvedValue({ result: 'success' });
32
45
 
@@ -90,7 +103,7 @@ describe('ToolsService', () => {
90
103
  );
91
104
 
92
105
  const discoveryRequest = createMockRequest({ path: '/discovery' });
93
- const response = await toolsService.processRequest(discoveryRequest);
106
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
94
107
 
95
108
  expect(response.status).toBe(200);
96
109
  expect(response).toHaveProperty('bodyJSON');
@@ -115,7 +128,7 @@ describe('ToolsService', () => {
115
128
 
116
129
  it('should return empty functions array when no tools are registered', async () => {
117
130
  const discoveryRequest = createMockRequest({ path: '/discovery' });
118
- const response = await toolsService.processRequest(discoveryRequest);
131
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
119
132
 
120
133
  expect(response.status).toBe(200);
121
134
  expect(response.bodyAsU8Array).toBeDefined();
@@ -149,7 +162,7 @@ describe('ToolsService', () => {
149
162
  );
150
163
 
151
164
  const discoveryRequest = createMockRequest({ path: '/discovery' });
152
- const response = await toolsService.processRequest(discoveryRequest);
165
+ const response = await toolsService.processRequest(discoveryRequest, mockToolFunction);
153
166
 
154
167
  expect(response.status).toBe(200);
155
168
 
@@ -195,15 +208,101 @@ describe('ToolsService', () => {
195
208
 
196
209
  it('should execute tool successfully with parameters', async () => {
197
210
  const mockRequest = createMockRequest();
198
- const response = await toolsService.processRequest(mockRequest);
211
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
199
212
 
200
213
  expect(response.status).toBe(200);
201
214
  expect(mockTool.handler).toHaveBeenCalledWith(
215
+ mockToolFunction, // functionContext
202
216
  { param1: 'test-value' },
203
217
  undefined
204
218
  );
205
219
  });
206
220
 
221
+ it('should execute tool with existing ToolFunction instance context', async () => {
222
+ // Create a mock ToolFunction instance
223
+ const mockToolFunctionInstance = {
224
+ someProperty: 'test-value',
225
+ someMethod: jest.fn(),
226
+ ready: jest.fn().mockResolvedValue(true),
227
+ perform: jest.fn(),
228
+ validateBearerToken: jest.fn().mockReturnValue(true),
229
+ request: {} as any
230
+ } as any;
231
+
232
+ const mockRequest = createMockRequest();
233
+ const response = await toolsService.processRequest(mockRequest, mockToolFunctionInstance);
234
+
235
+ expect(response.status).toBe(200);
236
+ expect(mockTool.handler).toHaveBeenCalledWith(
237
+ mockToolFunctionInstance, // functionContext - existing instance
238
+ { param1: 'test-value' },
239
+ undefined
240
+ );
241
+ });
242
+
243
+ it('should allow handler in ToolFunction subclass to access request object', async () => {
244
+ // Create a mock class that extends ToolFunction
245
+ class MockToolFunction extends ToolFunction {
246
+ public testMethod() {
247
+ return `path: ${this.request.path}`;
248
+ }
249
+
250
+ public getRequestPath() {
251
+ return this.request.path;
252
+ }
253
+ }
254
+
255
+ // Create an instance with a mock request
256
+ const mockRequest = createMockRequest({ path: '/test-path' });
257
+ const mockToolFunctionInstance = new MockToolFunction(mockRequest);
258
+
259
+ // Create a handler that will use the ToolFunction instance's methods and properties
260
+ const handlerThatAccessesRequest = jest.fn().mockImplementation((
261
+ functionContext: any,
262
+ params: any,
263
+ _authData: any
264
+ ) => {
265
+ // This simulates what would happen in a decorated method of a ToolFunction subclass
266
+ if (functionContext && functionContext instanceof MockToolFunction) {
267
+ return Promise.resolve({
268
+ success: true,
269
+ requestPath: functionContext.getRequestPath(), // Use public method to access request
270
+ testMethodResult: functionContext.testMethod(),
271
+ receivedParams: params
272
+ });
273
+ }
274
+ return Promise.resolve({ success: false, error: 'No valid function context' });
275
+ });
276
+
277
+ // Register a tool with our custom handler
278
+ toolsService.registerTool(
279
+ 'test-toolfunction-access',
280
+ 'Test handler access to ToolFunction instance',
281
+ handlerThatAccessesRequest,
282
+ [],
283
+ '/test-toolfunction-access'
284
+ );
285
+
286
+ const testRequest = createMockRequest({
287
+ path: '/test-toolfunction-access',
288
+ bodyJSON: { action: 'test' }
289
+ });
290
+
291
+ const response = await toolsService.processRequest(testRequest, mockToolFunctionInstance);
292
+
293
+ expect(response.status).toBe(200);
294
+ expect((response as any).data).toBeDefined();
295
+ expect((response as any).data.success).toBe(true);
296
+ expect((response as any).data.requestPath).toBe('/test-path');
297
+ expect((response as any).data.testMethodResult).toBe('path: /test-path');
298
+ expect((response as any).data.receivedParams).toEqual({ action: 'test' });
299
+ expect(handlerThatAccessesRequest).toHaveBeenCalledWith(
300
+ mockToolFunctionInstance, // functionContext is the ToolFunction instance
301
+ { action: 'test' },
302
+ undefined
303
+ );
304
+ });
305
+
207
306
  it('should execute tool with OptiID auth data when provided', async () => {
208
307
  const authData = new OptiIdAuthData(
209
308
  'optiId',
@@ -221,10 +320,11 @@ describe('ToolsService', () => {
221
320
  })
222
321
  });
223
322
 
224
- const response = await toolsService.processRequest(requestWithAuth);
323
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
225
324
 
226
325
  expect(response.status).toBe(200);
227
326
  expect(mockTool.handler).toHaveBeenCalledWith(
327
+ mockToolFunction, // functionContext
228
328
  { param1: 'test-value' },
229
329
  authData
230
330
  );
@@ -236,10 +336,11 @@ describe('ToolsService', () => {
236
336
  body: JSON.stringify({ param1: 'test-value' })
237
337
  });
238
338
 
239
- const response = await toolsService.processRequest(requestWithoutWrapper);
339
+ const response = await toolsService.processRequest(requestWithoutWrapper, mockToolFunction);
240
340
 
241
341
  expect(response.status).toBe(200);
242
342
  expect(mockTool.handler).toHaveBeenCalledWith(
343
+ mockToolFunction, // functionContext
243
344
  { param1: 'test-value' },
244
345
  undefined
245
346
  );
@@ -250,7 +351,7 @@ describe('ToolsService', () => {
250
351
  jest.mocked(mockTool.handler).mockRejectedValueOnce(new Error(errorMessage));
251
352
 
252
353
  const mockRequest = createMockRequest();
253
- const response = await toolsService.processRequest(mockRequest);
354
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
254
355
 
255
356
  expect(response.status).toBe(500);
256
357
  expect(logger.error).toHaveBeenCalledWith(
@@ -263,7 +364,7 @@ describe('ToolsService', () => {
263
364
  jest.mocked(mockTool.handler).mockRejectedValueOnce({});
264
365
 
265
366
  const mockRequest = createMockRequest();
266
- const response = await toolsService.processRequest(mockRequest);
367
+ const response = await toolsService.processRequest(mockRequest, mockToolFunction);
267
368
 
268
369
  expect(response.status).toBe(500);
269
370
  });
@@ -285,10 +386,10 @@ describe('ToolsService', () => {
285
386
  body: JSON.stringify({ data: { param1: 'test-value' } })
286
387
  });
287
388
 
288
- const response = await toolsService.processRequest(interactionRequest);
389
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
289
390
 
290
391
  expect(response.status).toBe(200);
291
- expect(mockInteraction.handler).toHaveBeenCalledWith({ param1: 'test-value' }, undefined);
392
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
292
393
  });
293
394
 
294
395
  it('should handle interaction request body without data wrapper', async () => {
@@ -298,10 +399,10 @@ describe('ToolsService', () => {
298
399
  body: JSON.stringify({ param1: 'test-value' })
299
400
  });
300
401
 
301
- const response = await toolsService.processRequest(interactionRequest);
402
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
302
403
 
303
404
  expect(response.status).toBe(200);
304
- expect(mockInteraction.handler).toHaveBeenCalledWith({ param1: 'test-value' }, undefined);
405
+ expect(mockInteraction.handler).toHaveBeenCalledWith(mockToolFunction, { param1: 'test-value' }, undefined);
305
406
  });
306
407
 
307
408
  it('should execute interaction with OptiID auth data when provided', async () => {
@@ -322,10 +423,11 @@ describe('ToolsService', () => {
322
423
  })
323
424
  });
324
425
 
325
- const response = await toolsService.processRequest(interactionRequest);
426
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
326
427
 
327
428
  expect(response.status).toBe(200);
328
429
  expect(mockInteraction.handler).toHaveBeenCalledWith(
430
+ mockToolFunction, // functionContext
329
431
  { param1: 'test-value' },
330
432
  authData
331
433
  );
@@ -349,10 +451,11 @@ describe('ToolsService', () => {
349
451
  })
350
452
  });
351
453
 
352
- const response = await toolsService.processRequest(interactionRequest);
454
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
353
455
 
354
456
  expect(response.status).toBe(200);
355
457
  expect(mockInteraction.handler).toHaveBeenCalledWith(
458
+ mockToolFunction, // functionContext
356
459
  {
357
460
  param1: 'test-value',
358
461
  auth: authData
@@ -370,7 +473,7 @@ describe('ToolsService', () => {
370
473
  bodyJSON: { data: { param1: 'test-value' } }
371
474
  });
372
475
 
373
- const response = await toolsService.processRequest(interactionRequest);
476
+ const response = await toolsService.processRequest(interactionRequest, mockToolFunction);
374
477
 
375
478
  expect(response.status).toBe(500);
376
479
  expect(logger.error).toHaveBeenCalledWith(
@@ -383,7 +486,7 @@ describe('ToolsService', () => {
383
486
  describe('error cases', () => {
384
487
  it('should return 404 when no matching tool or interaction is found', async () => {
385
488
  const unknownRequest = createMockRequest({ path: '/unknown-endpoint' });
386
- const response = await toolsService.processRequest(unknownRequest);
489
+ const response = await toolsService.processRequest(unknownRequest, mockToolFunction);
387
490
 
388
491
  expect(response.status).toBe(404);
389
492
  });
@@ -406,7 +509,7 @@ describe('ToolsService', () => {
406
509
  path: '/optid-auth-tool'
407
510
  });
408
511
 
409
- const response = await toolsService.processRequest(authRequest);
512
+ const response = await toolsService.processRequest(authRequest, mockToolFunction);
410
513
 
411
514
  expect(response.status).toBe(200);
412
515
  });
@@ -427,10 +530,10 @@ describe('ToolsService', () => {
427
530
  body: null
428
531
  });
429
532
 
430
- const response = await toolsService.processRequest(requestWithNullBody);
533
+ const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
431
534
 
432
535
  expect(response.status).toBe(200);
433
- expect(mockTool.handler).toHaveBeenCalledWith(null, undefined);
536
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
434
537
  });
435
538
 
436
539
  it('should handle request with undefined bodyJSON', async () => {
@@ -447,10 +550,10 @@ describe('ToolsService', () => {
447
550
  body: undefined
448
551
  });
449
552
 
450
- const response = await toolsService.processRequest(requestWithUndefinedBody);
553
+ const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
451
554
 
452
555
  expect(response.status).toBe(200);
453
- expect(mockTool.handler).toHaveBeenCalledWith(undefined, undefined);
556
+ expect(mockTool.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
454
557
  });
455
558
 
456
559
  it('should extract auth data from bodyJSON when body exists', async () => {
@@ -478,10 +581,11 @@ describe('ToolsService', () => {
478
581
  })
479
582
  });
480
583
 
481
- const response = await toolsService.processRequest(requestWithAuth);
584
+ const response = await toolsService.processRequest(requestWithAuth, mockToolFunction);
482
585
 
483
586
  expect(response.status).toBe(200);
484
587
  expect(mockTool.handler).toHaveBeenCalledWith(
588
+ mockToolFunction, // functionContext
485
589
  { param1: 'test-value' },
486
590
  authData
487
591
  );
@@ -506,10 +610,11 @@ describe('ToolsService', () => {
506
610
  })
507
611
  });
508
612
 
509
- const response = await toolsService.processRequest(requestWithoutAuth);
613
+ const response = await toolsService.processRequest(requestWithoutAuth, mockToolFunction);
510
614
 
511
615
  expect(response.status).toBe(200);
512
616
  expect(mockTool.handler).toHaveBeenCalledWith(
617
+ mockToolFunction, // functionContext
513
618
  { param1: 'test-value' },
514
619
  undefined
515
620
  );
@@ -537,10 +642,11 @@ describe('ToolsService', () => {
537
642
  body: ''
538
643
  });
539
644
 
540
- const response = await toolsService.processRequest(requestWithAuthButNoBody);
645
+ const response = await toolsService.processRequest(requestWithAuthButNoBody, mockToolFunction);
541
646
 
542
647
  expect(response.status).toBe(200);
543
648
  expect(mockTool.handler).toHaveBeenCalledWith(
649
+ mockToolFunction, // functionContext
544
650
  { param1: 'test-value' },
545
651
  authData
546
652
  );