@optimizely-opal/opal-tool-ocp-sdk 0.0.0-devmg.12 → 1.0.0-OCP-1441.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.
- package/README.md +108 -15
- package/dist/auth/AuthUtils.d.ts +26 -0
- package/dist/auth/AuthUtils.d.ts.map +1 -0
- package/dist/auth/AuthUtils.js +109 -0
- package/dist/auth/AuthUtils.js.map +1 -0
- package/dist/auth/AuthUtils.test.d.ts +2 -0
- package/dist/auth/AuthUtils.test.d.ts.map +1 -0
- package/dist/auth/AuthUtils.test.js +601 -0
- package/dist/auth/AuthUtils.test.js.map +1 -0
- package/dist/auth/TokenVerifier.d.ts.map +1 -1
- package/dist/auth/TokenVerifier.js +0 -1
- package/dist/auth/TokenVerifier.js.map +1 -1
- package/dist/auth/TokenVerifier.test.js +9 -0
- package/dist/auth/TokenVerifier.test.js.map +1 -1
- package/dist/function/GlobalToolFunction.d.ts +27 -0
- package/dist/function/GlobalToolFunction.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.js +53 -0
- package/dist/function/GlobalToolFunction.js.map +1 -0
- package/dist/function/GlobalToolFunction.test.d.ts +2 -0
- package/dist/function/GlobalToolFunction.test.d.ts.map +1 -0
- package/dist/function/GlobalToolFunction.test.js +425 -0
- package/dist/function/GlobalToolFunction.test.js.map +1 -0
- package/dist/function/ToolFunction.d.ts +1 -2
- package/dist/function/ToolFunction.d.ts.map +1 -1
- package/dist/function/ToolFunction.js +2 -36
- package/dist/function/ToolFunction.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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 +13 -0
- package/dist/service/Service.js.map +1 -1
- package/dist/service/Service.test.js +86 -4
- package/dist/service/Service.test.js.map +1 -1
- package/dist/validation/ParameterValidator.d.ts +42 -0
- package/dist/validation/ParameterValidator.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.js +122 -0
- package/dist/validation/ParameterValidator.js.map +1 -0
- package/dist/validation/ParameterValidator.test.d.ts +2 -0
- package/dist/validation/ParameterValidator.test.d.ts.map +1 -0
- package/dist/validation/ParameterValidator.test.js +282 -0
- package/dist/validation/ParameterValidator.test.js.map +1 -0
- package/package.json +3 -4
- package/src/auth/AuthUtils.test.ts +729 -0
- package/src/auth/AuthUtils.ts +117 -0
- package/src/auth/TokenVerifier.test.ts +11 -0
- package/src/auth/TokenVerifier.ts +0 -1
- package/src/function/GlobalToolFunction.test.ts +505 -0
- package/src/function/GlobalToolFunction.ts +56 -0
- package/src/function/ToolFunction.ts +3 -42
- package/src/index.ts +1 -0
- package/src/service/Service.test.ts +126 -12
- package/src/service/Service.ts +47 -9
- package/src/validation/ParameterValidator.test.ts +341 -0
- package/src/validation/ParameterValidator.ts +153 -0
- package/dist/function/ToolFunction.test.d.ts +0 -2
- package/dist/function/ToolFunction.test.d.ts.map +0 -1
- package/dist/function/ToolFunction.test.js +0 -314
- package/dist/function/ToolFunction.test.js.map +0 -1
- package/src/function/ToolFunction.test.ts +0 -374
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { Function, Response, amendLogContext
|
|
1
|
+
import { Function, Response, amendLogContext } from '@zaiusinc/app-sdk';
|
|
2
2
|
import { toolsService } from '../service/Service';
|
|
3
|
-
import { getTokenVerifier } from '../auth/TokenVerifier';
|
|
4
|
-
import { OptiIdAuthData } from '../types/Models';
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Abstract base class for tool-based function execution
|
|
@@ -41,46 +39,9 @@ export abstract class ToolFunction extends Function {
|
|
|
41
39
|
/**
|
|
42
40
|
* Authenticate the incoming request by validating the OptiID token and organization ID
|
|
43
41
|
*
|
|
44
|
-
* @
|
|
42
|
+
* @returns true if authentication succeeds
|
|
45
43
|
*/
|
|
46
44
|
private async authorizeRequest(): Promise<boolean> {
|
|
47
|
-
|
|
48
|
-
if (this.request.path === '/discovery' || this.request.path === '/ready') {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
const authData = this.request.bodyJSON?.auth as OptiIdAuthData;
|
|
52
|
-
const accessToken = authData?.credentials?.access_token;
|
|
53
|
-
if (!accessToken || authData?.provider?.toLowerCase() !== 'optiid') {
|
|
54
|
-
logger.error('OptiID token is required but not provided');
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const customerId = authData.credentials?.customer_id;
|
|
59
|
-
if (!customerId) {
|
|
60
|
-
logger.error('Organisation ID is required but not provided');
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const appOrganisationId = getAppContext().account?.organizationId;
|
|
65
|
-
if (customerId !== appOrganisationId) {
|
|
66
|
-
logger.error(`Invalid organisation ID: expected ${appOrganisationId}, received ${customerId}`);
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return await this.validateAccessToken(accessToken);
|
|
45
|
+
return true;
|
|
71
46
|
}
|
|
72
|
-
|
|
73
|
-
private async validateAccessToken(accessToken: string | undefined): Promise<boolean> {
|
|
74
|
-
try {
|
|
75
|
-
if (!accessToken) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
const tokenVerifier = await getTokenVerifier();
|
|
79
|
-
return await tokenVerifier.verify(accessToken);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
logger.error('OptiID token validation failed:', error);
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -522,15 +522,25 @@ describe('ToolsService', () => {
|
|
|
522
522
|
|
|
523
523
|
describe('edge cases', () => {
|
|
524
524
|
it('should handle request with null bodyJSON', async () => {
|
|
525
|
+
// Create a tool without required parameters
|
|
526
|
+
const toolWithoutRequiredParams = {
|
|
527
|
+
name: 'no_required_params_tool',
|
|
528
|
+
description: 'Tool without required parameters',
|
|
529
|
+
handler: jest.fn().mockResolvedValue({ result: 'success' }),
|
|
530
|
+
parameters: [], // No parameters defined
|
|
531
|
+
endpoint: '/no-required-params-tool'
|
|
532
|
+
};
|
|
533
|
+
|
|
525
534
|
toolsService.registerTool(
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
535
|
+
toolWithoutRequiredParams.name,
|
|
536
|
+
toolWithoutRequiredParams.description,
|
|
537
|
+
toolWithoutRequiredParams.handler,
|
|
538
|
+
toolWithoutRequiredParams.parameters,
|
|
539
|
+
toolWithoutRequiredParams.endpoint
|
|
531
540
|
);
|
|
532
541
|
|
|
533
542
|
const requestWithNullBody = createMockRequest({
|
|
543
|
+
path: '/no-required-params-tool',
|
|
534
544
|
bodyJSON: null,
|
|
535
545
|
body: null
|
|
536
546
|
});
|
|
@@ -538,19 +548,29 @@ describe('ToolsService', () => {
|
|
|
538
548
|
const response = await toolsService.processRequest(requestWithNullBody, mockToolFunction);
|
|
539
549
|
|
|
540
550
|
expect(response.status).toBe(200);
|
|
541
|
-
expect(
|
|
551
|
+
expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, null, undefined);
|
|
542
552
|
});
|
|
543
553
|
|
|
544
554
|
it('should handle request with undefined bodyJSON', async () => {
|
|
555
|
+
// Create a tool without required parameters
|
|
556
|
+
const toolWithoutRequiredParams = {
|
|
557
|
+
name: 'no_required_params_tool_2',
|
|
558
|
+
description: 'Tool without required parameters',
|
|
559
|
+
handler: jest.fn().mockResolvedValue({ result: 'success' }),
|
|
560
|
+
parameters: [], // No parameters defined
|
|
561
|
+
endpoint: '/no-required-params-tool-2'
|
|
562
|
+
};
|
|
563
|
+
|
|
545
564
|
toolsService.registerTool(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
565
|
+
toolWithoutRequiredParams.name,
|
|
566
|
+
toolWithoutRequiredParams.description,
|
|
567
|
+
toolWithoutRequiredParams.handler,
|
|
568
|
+
toolWithoutRequiredParams.parameters,
|
|
569
|
+
toolWithoutRequiredParams.endpoint
|
|
551
570
|
);
|
|
552
571
|
|
|
553
572
|
const requestWithUndefinedBody = createMockRequest({
|
|
573
|
+
path: '/no-required-params-tool-2',
|
|
554
574
|
bodyJSON: undefined,
|
|
555
575
|
body: undefined
|
|
556
576
|
});
|
|
@@ -558,7 +578,7 @@ describe('ToolsService', () => {
|
|
|
558
578
|
const response = await toolsService.processRequest(requestWithUndefinedBody, mockToolFunction);
|
|
559
579
|
|
|
560
580
|
expect(response.status).toBe(200);
|
|
561
|
-
expect(
|
|
581
|
+
expect(toolWithoutRequiredParams.handler).toHaveBeenCalledWith(mockToolFunction, undefined, undefined);
|
|
562
582
|
});
|
|
563
583
|
|
|
564
584
|
it('should extract auth data from bodyJSON when body exists', async () => {
|
|
@@ -657,5 +677,99 @@ describe('ToolsService', () => {
|
|
|
657
677
|
);
|
|
658
678
|
});
|
|
659
679
|
});
|
|
680
|
+
|
|
681
|
+
describe('parameter validation', () => {
|
|
682
|
+
beforeEach(() => {
|
|
683
|
+
jest.clearAllMocks();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should validate parameters and return 400 for invalid types', async () => {
|
|
687
|
+
// Register a tool with specific parameter types
|
|
688
|
+
const toolWithTypedParams = {
|
|
689
|
+
name: 'typed_tool',
|
|
690
|
+
description: 'Tool with typed parameters',
|
|
691
|
+
handler: jest.fn().mockResolvedValue({ result: 'success' }),
|
|
692
|
+
parameters: [
|
|
693
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
694
|
+
new Parameter('age', ParameterType.Integer, 'User age', true),
|
|
695
|
+
new Parameter('active', ParameterType.Boolean, 'Is active', false)
|
|
696
|
+
],
|
|
697
|
+
endpoint: '/typed-tool'
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
toolsService.registerTool(
|
|
701
|
+
toolWithTypedParams.name,
|
|
702
|
+
toolWithTypedParams.description,
|
|
703
|
+
toolWithTypedParams.handler,
|
|
704
|
+
toolWithTypedParams.parameters,
|
|
705
|
+
toolWithTypedParams.endpoint
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
// Send invalid parameter types
|
|
709
|
+
const invalidRequest = createMockRequest({
|
|
710
|
+
path: '/typed-tool',
|
|
711
|
+
bodyJSON: {
|
|
712
|
+
parameters: {
|
|
713
|
+
name: 123, // should be string
|
|
714
|
+
age: '25', // should be integer
|
|
715
|
+
active: 'true' // should be boolean
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const response = await toolsService.processRequest(invalidRequest, mockToolFunction);
|
|
721
|
+
|
|
722
|
+
expect(response.status).toBe(400);
|
|
723
|
+
|
|
724
|
+
// Expect simplified error format
|
|
725
|
+
expect(response.bodyJSON).toHaveProperty('errors');
|
|
726
|
+
expect(response.bodyJSON.errors).toHaveLength(3);
|
|
727
|
+
|
|
728
|
+
// Check error structure - only field and message
|
|
729
|
+
const errors = response.bodyJSON.errors;
|
|
730
|
+
expect(errors[0]).toHaveProperty('field', 'name');
|
|
731
|
+
expect(errors[0]).toHaveProperty('message', "Parameter 'name' must be a string, but received number");
|
|
732
|
+
|
|
733
|
+
// Verify the handler was not called
|
|
734
|
+
expect(toolWithTypedParams.handler).not.toHaveBeenCalled();
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should skip validation for tools with no parameter definitions', async () => {
|
|
738
|
+
const toolWithoutParams = {
|
|
739
|
+
name: 'no_params_tool',
|
|
740
|
+
description: 'Tool without parameters',
|
|
741
|
+
handler: jest.fn().mockResolvedValue({ result: 'success' }),
|
|
742
|
+
parameters: [], // No parameters defined
|
|
743
|
+
endpoint: '/no-params-tool'
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
toolsService.registerTool(
|
|
747
|
+
toolWithoutParams.name,
|
|
748
|
+
toolWithoutParams.description,
|
|
749
|
+
toolWithoutParams.handler,
|
|
750
|
+
toolWithoutParams.parameters,
|
|
751
|
+
toolWithoutParams.endpoint
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
// Send request with any data (should be ignored)
|
|
755
|
+
const request = createMockRequest({
|
|
756
|
+
path: '/no-params-tool',
|
|
757
|
+
bodyJSON: {
|
|
758
|
+
parameters: {
|
|
759
|
+
unexpected: 'value'
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const response = await toolsService.processRequest(request, mockToolFunction);
|
|
765
|
+
|
|
766
|
+
expect(response.status).toBe(200);
|
|
767
|
+
expect(toolWithoutParams.handler).toHaveBeenCalledWith(
|
|
768
|
+
mockToolFunction,
|
|
769
|
+
{ unexpected: 'value' },
|
|
770
|
+
undefined
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
});
|
|
660
774
|
});
|
|
661
775
|
});
|
package/src/service/Service.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { AuthRequirement, Parameter } from '../types/Models';
|
|
|
3
3
|
import * as App from '@zaiusinc/app-sdk';
|
|
4
4
|
import { logger } from '@zaiusinc/app-sdk';
|
|
5
5
|
import { ToolFunction } from '../function/ToolFunction';
|
|
6
|
+
import { GlobalToolFunction } from '../function/GlobalToolFunction';
|
|
7
|
+
import { ParameterValidator } from '../validation/ParameterValidator';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Default OptiID authentication requirement that will be enforced for all tools
|
|
9
11
|
*/
|
|
10
12
|
const DEFAULT_OPTIID_AUTH = new AuthRequirement('OptiID', 'default', true);
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
14
|
/**
|
|
15
15
|
* Result type for interaction handlers
|
|
16
16
|
*/
|
|
@@ -28,7 +28,11 @@ export class Interaction<TAuthData> {
|
|
|
28
28
|
public constructor(
|
|
29
29
|
public name: string,
|
|
30
30
|
public endpoint: string,
|
|
31
|
-
public handler: (
|
|
31
|
+
public handler: (
|
|
32
|
+
functionContext: ToolFunction | GlobalToolFunction,
|
|
33
|
+
data: unknown,
|
|
34
|
+
authData?: TAuthData
|
|
35
|
+
) => Promise<InteractionResult>
|
|
32
36
|
) {}
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -55,7 +59,11 @@ export class Tool<TAuthData> {
|
|
|
55
59
|
public description: string,
|
|
56
60
|
public parameters: Parameter[],
|
|
57
61
|
public endpoint: string,
|
|
58
|
-
public handler: (
|
|
62
|
+
public handler: (
|
|
63
|
+
functionContext: ToolFunction | GlobalToolFunction,
|
|
64
|
+
params: unknown,
|
|
65
|
+
authData?: TAuthData
|
|
66
|
+
) => Promise<unknown>,
|
|
59
67
|
public authRequirements: AuthRequirement[] = [DEFAULT_OPTIID_AUTH]
|
|
60
68
|
) {}
|
|
61
69
|
|
|
@@ -108,14 +116,25 @@ export class ToolsService {
|
|
|
108
116
|
public registerTool<TAuthData>(
|
|
109
117
|
name: string,
|
|
110
118
|
description: string,
|
|
111
|
-
handler: (
|
|
119
|
+
handler: (
|
|
120
|
+
functionContext: ToolFunction | GlobalToolFunction,
|
|
121
|
+
params: unknown,
|
|
122
|
+
authData?: TAuthData
|
|
123
|
+
) => Promise<unknown>,
|
|
112
124
|
parameters: Parameter[],
|
|
113
125
|
endpoint: string,
|
|
114
126
|
authRequirements?: AuthRequirement[]
|
|
115
127
|
): void {
|
|
116
128
|
// Enforce OptiID authentication for all tools
|
|
117
129
|
const enforcedAuthRequirements = this.enforceOptiIdAuth(authRequirements);
|
|
118
|
-
const func = new Tool<TAuthData>(
|
|
130
|
+
const func = new Tool<TAuthData>(
|
|
131
|
+
name,
|
|
132
|
+
description,
|
|
133
|
+
parameters,
|
|
134
|
+
endpoint,
|
|
135
|
+
handler,
|
|
136
|
+
enforcedAuthRequirements
|
|
137
|
+
);
|
|
119
138
|
this.functions.set(endpoint, func);
|
|
120
139
|
}
|
|
121
140
|
|
|
@@ -127,15 +146,21 @@ export class ToolsService {
|
|
|
127
146
|
*/
|
|
128
147
|
public registerInteraction<TAuthData>(
|
|
129
148
|
name: string,
|
|
130
|
-
handler: (
|
|
149
|
+
handler: (
|
|
150
|
+
functionContext: ToolFunction | GlobalToolFunction,
|
|
151
|
+
data: unknown,
|
|
152
|
+
authData?: TAuthData
|
|
153
|
+
) => Promise<InteractionResult>,
|
|
131
154
|
endpoint: string
|
|
132
155
|
): void {
|
|
133
156
|
const func = new Interaction<TAuthData>(name, endpoint, handler);
|
|
134
157
|
this.interactions.set(endpoint, func);
|
|
135
158
|
}
|
|
136
159
|
|
|
137
|
-
public async processRequest(
|
|
138
|
-
|
|
160
|
+
public async processRequest(
|
|
161
|
+
req: App.Request,
|
|
162
|
+
functionContext: ToolFunction | GlobalToolFunction
|
|
163
|
+
): Promise<App.Response> {
|
|
139
164
|
if (req.path === '/discovery') {
|
|
140
165
|
return new App.Response(200, { functions: Array.from(this.functions.values()).map((f) => f.toJSON()) });
|
|
141
166
|
} else {
|
|
@@ -149,6 +174,19 @@ export class ToolsService {
|
|
|
149
174
|
params = req.bodyJSON;
|
|
150
175
|
}
|
|
151
176
|
|
|
177
|
+
// Validate parameters before calling the handler (only if tool has parameter definitions)
|
|
178
|
+
if (func.parameters && func.parameters.length > 0) {
|
|
179
|
+
const validationResult = ParameterValidator.validate(params, func.parameters);
|
|
180
|
+
if (!validationResult.isValid) {
|
|
181
|
+
return new App.Response(400, {
|
|
182
|
+
errors: validationResult.errors.map((error) => ({
|
|
183
|
+
field: error.parameter,
|
|
184
|
+
message: error.message
|
|
185
|
+
}))
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
152
190
|
// Extract auth data from body JSON
|
|
153
191
|
const authData = req.bodyJSON ? req.bodyJSON.auth : undefined;
|
|
154
192
|
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { ParameterValidator } from './ParameterValidator';
|
|
2
|
+
import { Parameter, ParameterType } from '../types/Models';
|
|
3
|
+
|
|
4
|
+
describe('ParameterValidator', () => {
|
|
5
|
+
describe('validate', () => {
|
|
6
|
+
it('should pass validation for valid parameters', () => {
|
|
7
|
+
const paramDefs = [
|
|
8
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
9
|
+
new Parameter('age', ParameterType.Integer, 'User age', false),
|
|
10
|
+
new Parameter('score', ParameterType.Number, 'User score', false),
|
|
11
|
+
new Parameter('active', ParameterType.Boolean, 'Is active', false),
|
|
12
|
+
new Parameter('tags', ParameterType.List, 'User tags', false),
|
|
13
|
+
new Parameter('config', ParameterType.Dictionary, 'User config', false)
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const validParams = {
|
|
17
|
+
name: 'John Doe',
|
|
18
|
+
age: 25,
|
|
19
|
+
score: 85.5,
|
|
20
|
+
active: true,
|
|
21
|
+
tags: ['admin', 'user'],
|
|
22
|
+
config: { theme: 'dark', language: 'en' }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = ParameterValidator.validate(validParams, paramDefs);
|
|
26
|
+
|
|
27
|
+
expect(result.isValid).toBe(true);
|
|
28
|
+
expect(result.errors).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should fail validation for missing required parameters', () => {
|
|
32
|
+
const paramDefs = [
|
|
33
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
34
|
+
new Parameter('email', ParameterType.String, 'User email', true)
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const params = {
|
|
38
|
+
name: 'John Doe'
|
|
39
|
+
// email is missing
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
43
|
+
|
|
44
|
+
expect(result.isValid).toBe(false);
|
|
45
|
+
expect(result.errors).toHaveLength(1);
|
|
46
|
+
expect(result.errors[0]).toEqual({
|
|
47
|
+
parameter: 'email',
|
|
48
|
+
message: "Required parameter 'email' is missing"
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should fail validation for wrong parameter types', () => {
|
|
53
|
+
const paramDefs = [
|
|
54
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
55
|
+
new Parameter('age', ParameterType.Integer, 'User age', true),
|
|
56
|
+
new Parameter('active', ParameterType.Boolean, 'Is active', true),
|
|
57
|
+
new Parameter('tags', ParameterType.List, 'User tags', true),
|
|
58
|
+
new Parameter('config', ParameterType.Dictionary, 'User config', true)
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const invalidParams = {
|
|
62
|
+
name: 123, // should be string
|
|
63
|
+
age: '25', // should be integer
|
|
64
|
+
active: 'true', // should be boolean
|
|
65
|
+
tags: 'tag1,tag2', // should be array
|
|
66
|
+
config: 'invalid' // should be object
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = ParameterValidator.validate(invalidParams, paramDefs);
|
|
70
|
+
|
|
71
|
+
expect(result.isValid).toBe(false);
|
|
72
|
+
expect(result.errors).toHaveLength(5);
|
|
73
|
+
|
|
74
|
+
expect(result.errors[0].parameter).toBe('name');
|
|
75
|
+
expect(result.errors[0].message).toContain('must be a string');
|
|
76
|
+
|
|
77
|
+
expect(result.errors[1].parameter).toBe('age');
|
|
78
|
+
expect(result.errors[1].message).toContain('must be an integer');
|
|
79
|
+
|
|
80
|
+
expect(result.errors[2].parameter).toBe('active');
|
|
81
|
+
expect(result.errors[2].message).toContain('must be a boolean');
|
|
82
|
+
|
|
83
|
+
expect(result.errors[3].parameter).toBe('tags');
|
|
84
|
+
expect(result.errors[3].message).toContain('must be an array');
|
|
85
|
+
|
|
86
|
+
expect(result.errors[4].parameter).toBe('config');
|
|
87
|
+
expect(result.errors[4].message).toContain('must be an object');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle null/undefined parameters correctly', () => {
|
|
91
|
+
const paramDefs = [
|
|
92
|
+
new Parameter('required', ParameterType.String, 'Required param', true),
|
|
93
|
+
new Parameter('optional', ParameterType.String, 'Optional param', false)
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const result1 = ParameterValidator.validate(null, paramDefs);
|
|
97
|
+
expect(result1.isValid).toBe(false);
|
|
98
|
+
expect(result1.errors).toHaveLength(1);
|
|
99
|
+
expect(result1.errors[0].parameter).toBe('required');
|
|
100
|
+
|
|
101
|
+
const result2 = ParameterValidator.validate(undefined, paramDefs);
|
|
102
|
+
expect(result2.isValid).toBe(false);
|
|
103
|
+
expect(result2.errors).toHaveLength(1);
|
|
104
|
+
expect(result2.errors[0].parameter).toBe('required');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should allow optional parameters to be missing', () => {
|
|
108
|
+
const paramDefs = [
|
|
109
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
110
|
+
new Parameter('age', ParameterType.Integer, 'User age', false)
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const params = {
|
|
114
|
+
name: 'John Doe'
|
|
115
|
+
// age is optional and missing
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
119
|
+
|
|
120
|
+
expect(result.isValid).toBe(true);
|
|
121
|
+
expect(result.errors).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should distinguish between integer and number types', () => {
|
|
125
|
+
const paramDefs = [
|
|
126
|
+
new Parameter('count', ParameterType.Integer, 'Item count', true),
|
|
127
|
+
new Parameter('score', ParameterType.Number, 'Score value', true)
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const params1 = {
|
|
131
|
+
count: 25.5, // should be integer, not float
|
|
132
|
+
score: 85.5 // number is fine
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const result1 = ParameterValidator.validate(params1, paramDefs);
|
|
136
|
+
expect(result1.isValid).toBe(false);
|
|
137
|
+
expect(result1.errors).toHaveLength(1);
|
|
138
|
+
expect(result1.errors[0].parameter).toBe('count');
|
|
139
|
+
expect(result1.errors[0].message).toContain('must be an integer');
|
|
140
|
+
|
|
141
|
+
const params2 = {
|
|
142
|
+
count: 25, // integer is fine
|
|
143
|
+
score: 85.5 // number is fine
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const result2 = ParameterValidator.validate(params2, paramDefs);
|
|
147
|
+
expect(result2.isValid).toBe(true);
|
|
148
|
+
expect(result2.errors).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should handle array vs object distinction', () => {
|
|
152
|
+
const paramDefs = [
|
|
153
|
+
new Parameter('tags', ParameterType.List, 'Tag list', true),
|
|
154
|
+
new Parameter('config', ParameterType.Dictionary, 'Config object', true)
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const params = {
|
|
158
|
+
tags: { 0: 'tag1', 1: 'tag2' }, // object that looks like array
|
|
159
|
+
config: ['key1', 'key2'] // array instead of object
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
163
|
+
expect(result.isValid).toBe(false);
|
|
164
|
+
expect(result.errors).toHaveLength(2);
|
|
165
|
+
|
|
166
|
+
expect(result.errors[0].parameter).toBe('tags');
|
|
167
|
+
expect(result.errors[0].message).toContain('must be an array');
|
|
168
|
+
|
|
169
|
+
expect(result.errors[1].parameter).toBe('config');
|
|
170
|
+
expect(result.errors[1].message).toContain('must be an object');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle null values for optional parameters', () => {
|
|
174
|
+
const paramDefs = [
|
|
175
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
176
|
+
new Parameter('age', ParameterType.Integer, 'User age', false)
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const params = {
|
|
180
|
+
name: 'John Doe',
|
|
181
|
+
age: null // null for optional parameter should be allowed
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
185
|
+
expect(result.isValid).toBe(true);
|
|
186
|
+
expect(result.errors).toHaveLength(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle edge cases for number validation', () => {
|
|
190
|
+
const paramDefs = [
|
|
191
|
+
new Parameter('score', ParameterType.Number, 'Score value', true)
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
// Test NaN
|
|
195
|
+
const params1 = { score: NaN };
|
|
196
|
+
const result1 = ParameterValidator.validate(params1, paramDefs);
|
|
197
|
+
expect(result1.isValid).toBe(false);
|
|
198
|
+
expect(result1.errors[0].message).toContain('must be a number');
|
|
199
|
+
|
|
200
|
+
// Test Infinity
|
|
201
|
+
const params2 = { score: Infinity };
|
|
202
|
+
const result2 = ParameterValidator.validate(params2, paramDefs);
|
|
203
|
+
expect(result2.isValid).toBe(true);
|
|
204
|
+
expect(result2.errors).toHaveLength(0);
|
|
205
|
+
|
|
206
|
+
// Test negative numbers
|
|
207
|
+
const params3 = { score: -42.5 };
|
|
208
|
+
const result3 = ParameterValidator.validate(params3, paramDefs);
|
|
209
|
+
expect(result3.isValid).toBe(true);
|
|
210
|
+
expect(result3.errors).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle edge cases for integer validation', () => {
|
|
214
|
+
const paramDefs = [
|
|
215
|
+
new Parameter('count', ParameterType.Integer, 'Item count', true)
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
// Test negative integers
|
|
219
|
+
const params1 = { count: -5 };
|
|
220
|
+
const result1 = ParameterValidator.validate(params1, paramDefs);
|
|
221
|
+
expect(result1.isValid).toBe(true);
|
|
222
|
+
expect(result1.errors).toHaveLength(0);
|
|
223
|
+
|
|
224
|
+
// Test zero
|
|
225
|
+
const params2 = { count: 0 };
|
|
226
|
+
const result2 = ParameterValidator.validate(params2, paramDefs);
|
|
227
|
+
expect(result2.isValid).toBe(true);
|
|
228
|
+
expect(result2.errors).toHaveLength(0);
|
|
229
|
+
|
|
230
|
+
// Test very large integers
|
|
231
|
+
const params3 = { count: Number.MAX_SAFE_INTEGER };
|
|
232
|
+
const result3 = ParameterValidator.validate(params3, paramDefs);
|
|
233
|
+
expect(result3.isValid).toBe(true);
|
|
234
|
+
expect(result3.errors).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle empty arrays and objects', () => {
|
|
238
|
+
const paramDefs = [
|
|
239
|
+
new Parameter('tags', ParameterType.List, 'Tag list', true),
|
|
240
|
+
new Parameter('config', ParameterType.Dictionary, 'Config object', true)
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const params = {
|
|
244
|
+
tags: [], // empty array should be valid
|
|
245
|
+
config: {} // empty object should be valid
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
249
|
+
expect(result.isValid).toBe(true);
|
|
250
|
+
expect(result.errors).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle special string values', () => {
|
|
254
|
+
const paramDefs = [
|
|
255
|
+
new Parameter('text', ParameterType.String, 'Text value', true)
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// Test empty string
|
|
259
|
+
const params1 = { text: '' };
|
|
260
|
+
const result1 = ParameterValidator.validate(params1, paramDefs);
|
|
261
|
+
expect(result1.isValid).toBe(true);
|
|
262
|
+
expect(result1.errors).toHaveLength(0);
|
|
263
|
+
|
|
264
|
+
// Test string with special characters
|
|
265
|
+
const params2 = { text: 'Hello\nWorld\t!' };
|
|
266
|
+
const result2 = ParameterValidator.validate(params2, paramDefs);
|
|
267
|
+
expect(result2.isValid).toBe(true);
|
|
268
|
+
expect(result2.errors).toHaveLength(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle multiple validation errors', () => {
|
|
272
|
+
const paramDefs = [
|
|
273
|
+
new Parameter('name', ParameterType.String, 'User name', true),
|
|
274
|
+
new Parameter('age', ParameterType.Integer, 'User age', true),
|
|
275
|
+
new Parameter('email', ParameterType.String, 'User email', true)
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const params = {
|
|
279
|
+
name: 123, // wrong type
|
|
280
|
+
age: '25' // wrong type
|
|
281
|
+
// email is missing
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
285
|
+
expect(result.isValid).toBe(false);
|
|
286
|
+
expect(result.errors).toHaveLength(3);
|
|
287
|
+
|
|
288
|
+
expect(result.errors.some((e) => e.parameter === 'name')).toBe(true);
|
|
289
|
+
expect(result.errors.some((e) => e.parameter === 'age')).toBe(true);
|
|
290
|
+
expect(result.errors.some((e) => e.parameter === 'email')).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle tools with no parameter definitions', () => {
|
|
294
|
+
const result = ParameterValidator.validate({ someParam: 'value' }, []);
|
|
295
|
+
expect(result.isValid).toBe(true);
|
|
296
|
+
expect(result.errors).toHaveLength(0);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should handle extra parameters not in definition', () => {
|
|
300
|
+
const paramDefs = [
|
|
301
|
+
new Parameter('name', ParameterType.String, 'User name', true)
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const params = {
|
|
305
|
+
name: 'John Doe',
|
|
306
|
+
extraParam: 'should be ignored'
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
310
|
+
expect(result.isValid).toBe(true);
|
|
311
|
+
expect(result.errors).toHaveLength(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle nested objects and arrays', () => {
|
|
315
|
+
const paramDefs = [
|
|
316
|
+
new Parameter('config', ParameterType.Dictionary, 'Config object', true),
|
|
317
|
+
new Parameter('matrix', ParameterType.List, 'Matrix data', true)
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const params = {
|
|
321
|
+
config: {
|
|
322
|
+
nested: {
|
|
323
|
+
deep: {
|
|
324
|
+
value: 'test'
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
array: [1, 2, 3]
|
|
328
|
+
},
|
|
329
|
+
matrix: [
|
|
330
|
+
[1, 2, 3],
|
|
331
|
+
[4, 5, 6],
|
|
332
|
+
{ nested: 'object in array' }
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const result = ParameterValidator.validate(params, paramDefs);
|
|
337
|
+
expect(result.isValid).toBe(true);
|
|
338
|
+
expect(result.errors).toHaveLength(0);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|