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