@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.
- package/README.md +113 -24
- package/dist/decorator/Decorator.d.ts +4 -2
- package/dist/decorator/Decorator.d.ts.map +1 -1
- package/dist/decorator/Decorator.js +26 -4
- package/dist/decorator/Decorator.js.map +1 -1
- package/dist/decorator/Decorator.test.js +110 -0
- package/dist/decorator/Decorator.test.js.map +1 -1
- package/dist/function/ToolFunction.d.ts +17 -1
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +28 -1
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/function/ToolFunction.test.js +150 -6
- package/dist/function/ToolFunction.test.js.map +1 -1
- package/dist/service/Service.d.ts +8 -7
- package/dist/service/Service.d.ts.map +1 -1
- package/dist/service/Service.js +3 -3
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +114 -33
- package/dist/service/Service.test.js.map +1 -1
- package/package.json +8 -3
- package/src/decorator/Decorator.test.ts +126 -0
- package/src/decorator/Decorator.ts +32 -4
- package/src/function/ToolFunction.test.ts +176 -6
- package/src/function/ToolFunction.ts +30 -3
- package/src/service/Service.test.ts +131 -25
- package/src/service/Service.ts +9 -7
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|