mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(Chat Trigger Node): Prevent XSS vulnerabilities and improve parameter validation (#18148)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,7 @@ export { ExpressionExtensions } from './extensions';
|
||||
export * as ExpressionParser from './extensions/expression-parser';
|
||||
export { NativeMethods } from './native-methods';
|
||||
export * from './node-parameters/filter-parameter';
|
||||
export * from './node-parameters/parameter-type-validation';
|
||||
export * from './evaluation-helpers';
|
||||
|
||||
export type {
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
import { NodeOperationError } from '../errors';
|
||||
import type { INode } from '../interfaces';
|
||||
import { assert } from '../utils';
|
||||
|
||||
type ParameterType =
|
||||
| 'string'
|
||||
| 'boolean'
|
||||
| 'number'
|
||||
| 'resource-locator'
|
||||
| 'string[]'
|
||||
| 'number[]'
|
||||
| 'boolean[]'
|
||||
| 'object';
|
||||
|
||||
function assertUserInput<T>(condition: T, message: string, node: INode): asserts condition {
|
||||
try {
|
||||
assert(condition, message);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
// Use level 'info' to prevent reporting to Sentry (only 'error' and 'fatal' levels are reported)
|
||||
const nodeError = new NodeOperationError(node, e.message, { level: 'info' });
|
||||
nodeError.stack = e.stack;
|
||||
throw nodeError;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function assertParamIsType<T>(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
type: 'string' | 'number' | 'boolean',
|
||||
node: INode,
|
||||
): asserts value is T {
|
||||
assertUserInput(typeof value === type, `Parameter "${parameterName}" is not ${type}`, node);
|
||||
}
|
||||
|
||||
export function assertParamIsNumber(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
node: INode,
|
||||
): asserts value is number {
|
||||
assertParamIsType<number>(parameterName, value, 'number', node);
|
||||
}
|
||||
|
||||
export function assertParamIsString(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
node: INode,
|
||||
): asserts value is string {
|
||||
assertParamIsType<string>(parameterName, value, 'string', node);
|
||||
}
|
||||
|
||||
export function assertParamIsBoolean(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
node: INode,
|
||||
): asserts value is boolean {
|
||||
assertParamIsType<boolean>(parameterName, value, 'boolean', node);
|
||||
}
|
||||
|
||||
export function assertParamIsArray<T>(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
validator: (val: unknown) => val is T,
|
||||
node: INode,
|
||||
): asserts value is T[] {
|
||||
assertUserInput(Array.isArray(value), `Parameter "${parameterName}" is not an array`, node);
|
||||
|
||||
// Use for loop instead of .every() to properly handle sparse arrays
|
||||
// .every() skips empty/sparse indices, which could allow invalid arrays to pass
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (!validator(value[i])) {
|
||||
assertUserInput(
|
||||
false,
|
||||
`Parameter "${parameterName}" has elements that don't match expected types`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertIsValidObject(
|
||||
value: unknown,
|
||||
node: INode,
|
||||
): asserts value is Record<string, unknown> {
|
||||
assertUserInput(typeof value === 'object' && value !== null, 'Value is not a valid object', node);
|
||||
}
|
||||
|
||||
function assertIsRequiredParameter(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
isRequired: boolean,
|
||||
node: INode,
|
||||
): void {
|
||||
if (isRequired && value === undefined) {
|
||||
assertUserInput(false, `Required parameter "${parameterName}" is missing`, node);
|
||||
}
|
||||
}
|
||||
|
||||
function assertIsResourceLocator(parameterName: string, value: unknown, node: INode): void {
|
||||
assertUserInput(
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'__rl' in value &&
|
||||
'mode' in value &&
|
||||
'value' in value,
|
||||
`Parameter "${parameterName}" is not a valid resource locator object`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
|
||||
function assertParamIsObject(parameterName: string, value: unknown, node: INode): void {
|
||||
assertUserInput(
|
||||
typeof value === 'object' && value !== null,
|
||||
`Parameter "${parameterName}" is not a valid object`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
|
||||
function createElementValidator<T extends 'string' | 'number' | 'boolean'>(elementType: T) {
|
||||
return (
|
||||
val: unknown,
|
||||
): val is T extends 'string' ? string : T extends 'number' ? number : boolean =>
|
||||
typeof val === elementType;
|
||||
}
|
||||
|
||||
function assertParamIsArrayOfType(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
arrayType: string,
|
||||
node: INode,
|
||||
): void {
|
||||
const baseType = arrayType.slice(0, -2);
|
||||
const elementType =
|
||||
baseType === 'string' || baseType === 'number' || baseType === 'boolean' ? baseType : 'string';
|
||||
|
||||
const validator = createElementValidator(elementType);
|
||||
assertParamIsArray(parameterName, value, validator, node);
|
||||
}
|
||||
|
||||
function assertParamIsPrimitive(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
type: string,
|
||||
node: INode,
|
||||
): void {
|
||||
assertUserInput(
|
||||
typeof value === type,
|
||||
`Parameter "${parameterName}" is not a valid ${type}`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
|
||||
function validateParameterType(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
type: ParameterType,
|
||||
node: INode,
|
||||
): boolean {
|
||||
try {
|
||||
if (type === 'resource-locator') {
|
||||
assertIsResourceLocator(parameterName, value, node);
|
||||
} else if (type === 'object') {
|
||||
assertParamIsObject(parameterName, value, node);
|
||||
} else if (type.endsWith('[]')) {
|
||||
assertParamIsArrayOfType(parameterName, value, type, node);
|
||||
} else {
|
||||
assertParamIsPrimitive(parameterName, value, type, node);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function validateParameterAgainstTypes(
|
||||
parameterName: string,
|
||||
value: unknown,
|
||||
types: ParameterType[],
|
||||
node: INode,
|
||||
): void {
|
||||
let isValid = false;
|
||||
|
||||
for (const type of types) {
|
||||
if (validateParameterType(parameterName, value, type, node)) {
|
||||
isValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
const typeList = types.join(' or ');
|
||||
assertUserInput(
|
||||
false,
|
||||
`Parameter "${parameterName}" does not match any of the expected types: ${typeList}`,
|
||||
node,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type InferParameterType<T extends ParameterType | ParameterType[]> = T extends ParameterType[]
|
||||
? InferSingleParameterType<T[number]>
|
||||
: T extends ParameterType
|
||||
? InferSingleParameterType<T>
|
||||
: never;
|
||||
|
||||
type InferSingleParameterType<T extends ParameterType> = T extends 'string'
|
||||
? string
|
||||
: T extends 'boolean'
|
||||
? boolean
|
||||
: T extends 'number'
|
||||
? number
|
||||
: T extends 'resource-locator'
|
||||
? Record<string, unknown>
|
||||
: T extends 'string[]'
|
||||
? string[]
|
||||
: T extends 'number[]'
|
||||
? number[]
|
||||
: T extends 'boolean[]'
|
||||
? boolean[]
|
||||
: T extends 'object'
|
||||
? Record<string, unknown>
|
||||
: unknown;
|
||||
|
||||
export function validateNodeParameters<
|
||||
T extends Record<string, { type: ParameterType | ParameterType[]; required?: boolean }>,
|
||||
>(
|
||||
value: unknown,
|
||||
parameters: T,
|
||||
node: INode,
|
||||
): asserts value is {
|
||||
[K in keyof T]: T[K]['required'] extends true
|
||||
? InferParameterType<T[K]['type']>
|
||||
: InferParameterType<T[K]['type']> | undefined;
|
||||
} {
|
||||
assertIsValidObject(value, node);
|
||||
|
||||
Object.keys(parameters).forEach((key) => {
|
||||
const param = parameters[key];
|
||||
const paramValue = value[key];
|
||||
|
||||
assertIsRequiredParameter(key, paramValue, param.required ?? false, node);
|
||||
|
||||
// If required, value cannot be undefined and must be validated
|
||||
// If not required, value can be undefined but should be validated when present
|
||||
if (param.required || paramValue !== undefined) {
|
||||
const types = Array.isArray(param.type) ? param.type : [param.type];
|
||||
validateParameterAgainstTypes(key, paramValue, types, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
import {
|
||||
validateNodeParameters,
|
||||
assertParamIsString,
|
||||
assertParamIsNumber,
|
||||
assertParamIsBoolean,
|
||||
assertParamIsArray,
|
||||
} from '../../src/node-parameters/parameter-type-validation';
|
||||
import type { INode } from '../../src/interfaces';
|
||||
|
||||
describe('Type assertion functions', () => {
|
||||
const mockNode: INode = {
|
||||
id: 'test-node-id',
|
||||
name: 'TestNode',
|
||||
type: 'n8n-nodes-base.testNode',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
describe('assertIsNodeParameters', () => {
|
||||
it('should pass for valid object with all required parameters', () => {
|
||||
const value = {
|
||||
name: 'test',
|
||||
age: 25,
|
||||
active: true,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
age: { type: 'number' as const, required: true },
|
||||
active: { type: 'boolean' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for valid object with optional parameters present', () => {
|
||||
const value = {
|
||||
name: 'test',
|
||||
description: 'optional description',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
description: { type: 'string' as const },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for valid object with optional parameters missing', () => {
|
||||
const value = {
|
||||
name: 'test',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
description: { type: 'string' as const },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for valid array parameters', () => {
|
||||
const value = {
|
||||
tags: ['tag1', 'tag2'],
|
||||
numbers: [1, 2, 3],
|
||||
flags: [true, false],
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
tags: { type: 'string[]' as const, required: true },
|
||||
numbers: { type: 'number[]' as const, required: true },
|
||||
flags: { type: 'boolean[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for valid resource-locator parameter', () => {
|
||||
const value = {
|
||||
resource: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'some-value',
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for valid object parameter', () => {
|
||||
const value = {
|
||||
config: {
|
||||
setting1: 'value1',
|
||||
setting2: 42,
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
config: { type: 'object' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should pass for parameter with multiple allowed types', () => {
|
||||
const value = {
|
||||
multiType: 'string value',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
multiType: { type: ['string', 'number'] as Array<'string' | 'number'>, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
|
||||
// Test with number value
|
||||
const value2 = {
|
||||
multiType: 42,
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value2, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for null value', () => {
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(null, parameters, mockNode)).toThrow(
|
||||
'Value is not a valid object',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for non-object value', () => {
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters('not an object', parameters, mockNode)).toThrow(
|
||||
'Value is not a valid object',
|
||||
);
|
||||
expect(() => validateNodeParameters(123, parameters, mockNode)).toThrow(
|
||||
'Value is not a valid object',
|
||||
);
|
||||
expect(() => validateNodeParameters(true, parameters, mockNode)).toThrow(
|
||||
'Value is not a valid object',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for missing required parameter', () => {
|
||||
const value = {
|
||||
// name is missing
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Required parameter "name" is missing',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for parameter with wrong type', () => {
|
||||
const value = {
|
||||
name: 123, // should be string
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "name" does not match any of the expected types: string',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for invalid array parameter', () => {
|
||||
const value = {
|
||||
tags: 'not an array',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
tags: { type: 'string[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "tags" does not match any of the expected types: string[]',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for array with wrong element type', () => {
|
||||
const value = {
|
||||
tags: ['valid', 123, 'also valid'], // 123 is not a string
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
tags: { type: 'string[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "tags" does not match any of the expected types: string[]',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for invalid resource-locator parameter', () => {
|
||||
const value = {
|
||||
resource: {
|
||||
// missing required properties
|
||||
mode: 'list',
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "resource" does not match any of the expected types: resource-locator',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for invalid object parameter', () => {
|
||||
const value = {
|
||||
config: 'not an object',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
config: { type: 'object' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "config" does not match any of the expected types: object',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for parameter that matches none of the allowed types', () => {
|
||||
const value = {
|
||||
multiType: true, // should be string or number
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
multiType: { type: ['string', 'number'] as Array<'string' | 'number'>, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "multiType" does not match any of the expected types: string or number',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty parameter definition', () => {
|
||||
const value = {
|
||||
extra: 'should be ignored',
|
||||
};
|
||||
|
||||
const parameters = {};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle complex nested scenarios', () => {
|
||||
const value = {
|
||||
name: 'test',
|
||||
tags: ['tag1', 'tag2'],
|
||||
config: {
|
||||
enabled: true,
|
||||
timeout: 5000,
|
||||
},
|
||||
resource: {
|
||||
__rl: true,
|
||||
mode: 'id',
|
||||
value: '12345',
|
||||
},
|
||||
optionalField: undefined,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
tags: { type: 'string[]' as const, required: true },
|
||||
config: { type: 'object' as const, required: true },
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
optionalField: { type: 'string' as const },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle empty arrays', () => {
|
||||
const value = {
|
||||
emptyTags: [],
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
emptyTags: { type: 'string[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle null values for optional parameters', () => {
|
||||
const value = {
|
||||
name: 'test',
|
||||
optionalField: null,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
optionalField: { type: 'string' as const },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "optionalField" does not match any of the expected types: string',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle resource-locator with additional properties', () => {
|
||||
const value = {
|
||||
resource: {
|
||||
__rl: true,
|
||||
mode: 'list',
|
||||
value: 'some-value',
|
||||
extraProperty: 'ignored',
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertParamIsBoolean', () => {
|
||||
it('should pass for valid boolean values', () => {
|
||||
expect(() => assertParamIsBoolean('testParam', true, mockNode)).not.toThrow();
|
||||
expect(() => assertParamIsBoolean('testParam', false, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for non-boolean values', () => {
|
||||
expect(() => assertParamIsBoolean('testParam', 'true', mockNode)).toThrow(
|
||||
'Parameter "testParam" is not boolean',
|
||||
);
|
||||
expect(() => assertParamIsBoolean('testParam', 1, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not boolean',
|
||||
);
|
||||
expect(() => assertParamIsBoolean('testParam', 0, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not boolean',
|
||||
);
|
||||
expect(() => assertParamIsBoolean('testParam', null, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not boolean',
|
||||
);
|
||||
expect(() => assertParamIsBoolean('testParam', undefined, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not boolean',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertIsString', () => {
|
||||
it('should pass for valid string', () => {
|
||||
expect(() => assertParamIsString('testParam', 'hello', mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for non-string values', () => {
|
||||
expect(() => assertParamIsString('testParam', 123, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not string',
|
||||
);
|
||||
expect(() => assertParamIsString('testParam', true, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not string',
|
||||
);
|
||||
expect(() => assertParamIsString('testParam', null, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not string',
|
||||
);
|
||||
expect(() => assertParamIsString('testParam', undefined, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not string',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertIsNumber', () => {
|
||||
it('should pass for valid number', () => {
|
||||
expect(() => assertParamIsNumber('testParam', 123, mockNode)).not.toThrow();
|
||||
expect(() => assertParamIsNumber('testParam', 0, mockNode)).not.toThrow();
|
||||
expect(() => assertParamIsNumber('testParam', -5.5, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for non-number values', () => {
|
||||
expect(() => assertParamIsNumber('testParam', '123', mockNode)).toThrow(
|
||||
'Parameter "testParam" is not number',
|
||||
);
|
||||
expect(() => assertParamIsNumber('testParam', true, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not number',
|
||||
);
|
||||
expect(() => assertParamIsNumber('testParam', null, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not number',
|
||||
);
|
||||
expect(() => assertParamIsNumber('testParam', undefined, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not number',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertIsArray', () => {
|
||||
const isString = (val: unknown): val is string => typeof val === 'string';
|
||||
const isNumber = (val: unknown): val is number => typeof val === 'number';
|
||||
|
||||
it('should pass for valid array with correct element types', () => {
|
||||
expect(() =>
|
||||
assertParamIsArray('testParam', ['a', 'b', 'c'], isString, mockNode),
|
||||
).not.toThrow();
|
||||
expect(() => assertParamIsArray('testParam', [1, 2, 3], isNumber, mockNode)).not.toThrow();
|
||||
expect(() => assertParamIsArray('testParam', [], isString, mockNode)).not.toThrow(); // empty array
|
||||
});
|
||||
|
||||
it('should throw for non-array values', () => {
|
||||
expect(() => assertParamIsArray('testParam', 'not array', isString, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not an array',
|
||||
);
|
||||
expect(() => assertParamIsArray('testParam', { length: 3 }, isString, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not an array',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for array with incorrect element types', () => {
|
||||
expect(() => assertParamIsArray('testParam', ['a', 1, 'c'], isString, mockNode)).toThrow(
|
||||
'Parameter "testParam" has elements that don\'t match expected types',
|
||||
);
|
||||
expect(() => assertParamIsArray('testParam', [1, 'b', 3], isNumber, mockNode)).toThrow(
|
||||
'Parameter "testParam" has elements that don\'t match expected types',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases and additional scenarios', () => {
|
||||
describe('validateNodeParameters edge cases', () => {
|
||||
it('should handle NaN values correctly', () => {
|
||||
const value = {
|
||||
number: NaN,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
number: { type: 'number' as const, required: true },
|
||||
};
|
||||
|
||||
// NaN is of type 'number' in JavaScript
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle Infinity values correctly', () => {
|
||||
const value = {
|
||||
number: Infinity,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
number: { type: 'number' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle mixed array types correctly', () => {
|
||||
const value = {
|
||||
mixed: [1, '2', 3], // Invalid: mixed types in array
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
mixed: { type: 'number[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "mixed" does not match any of the expected types: number[]',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle nested arrays', () => {
|
||||
const value = {
|
||||
nested: [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
], // Array of arrays
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
nested: { type: 'object' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle resource-locator with false __rl property', () => {
|
||||
const value = {
|
||||
resource: {
|
||||
__rl: false, // Should still be valid as it has the property
|
||||
mode: 'list',
|
||||
value: 'some-value',
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle resource-locator missing __rl property', () => {
|
||||
const value = {
|
||||
resource: {
|
||||
mode: 'list',
|
||||
value: 'some-value',
|
||||
// __rl is missing
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
resource: { type: 'resource-locator' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).toThrow(
|
||||
'Parameter "resource" does not match any of the expected types: resource-locator',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty string as valid string parameter', () => {
|
||||
const value = {
|
||||
name: '',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle zero as valid number parameter', () => {
|
||||
const value = {
|
||||
count: 0,
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
count: { type: 'number' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle arrays with only false values', () => {
|
||||
const value = {
|
||||
flags: [false, false, false],
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
flags: { type: 'boolean[]' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle three or more type unions', () => {
|
||||
const value = {
|
||||
multiType: 'string value',
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
multiType: {
|
||||
type: ['string', 'number', 'boolean'] as Array<'string' | 'number' | 'boolean'>,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
|
||||
// Test with boolean value
|
||||
const value2 = {
|
||||
multiType: true,
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value2, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle array types in multi-type parameters', () => {
|
||||
const value = {
|
||||
flexParam: ['a', 'b', 'c'],
|
||||
};
|
||||
|
||||
const parameters = {
|
||||
flexParam: {
|
||||
type: ['string', 'string[]'] as Array<'string' | 'string[]'>,
|
||||
required: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
|
||||
// Test with single string
|
||||
const value2 = {
|
||||
flexParam: 'single string',
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value2, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle object with null prototype', () => {
|
||||
const value = Object.create(null);
|
||||
value.name = 'test';
|
||||
|
||||
const parameters = {
|
||||
name: { type: 'string' as const, required: true },
|
||||
};
|
||||
|
||||
expect(() => validateNodeParameters(value, parameters, mockNode)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertParamIsArray edge cases', () => {
|
||||
const isString = (val: unknown): val is string => typeof val === 'string';
|
||||
|
||||
it('should handle array-like objects', () => {
|
||||
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
|
||||
|
||||
expect(() => assertParamIsArray('testParam', arrayLike, isString, mockNode)).toThrow(
|
||||
'Parameter "testParam" is not an array',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle sparse arrays', () => {
|
||||
const sparse = new Array(3);
|
||||
sparse[0] = 'a';
|
||||
sparse[2] = 'c';
|
||||
// sparse[1] is undefined
|
||||
|
||||
// For loop implementation properly validates sparse arrays and throws on undefined elements
|
||||
expect(() => assertParamIsArray('testParam', sparse, isString, mockNode)).toThrow(
|
||||
'Parameter "testParam" has elements that don\'t match expected types',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle arrays with explicit undefined values', () => {
|
||||
const arrayWithUndefined = ['a', undefined, 'c'];
|
||||
|
||||
expect(() =>
|
||||
assertParamIsArray('testParam', arrayWithUndefined, isString, mockNode),
|
||||
).toThrow('Parameter "testParam" has elements that don\'t match expected types');
|
||||
});
|
||||
|
||||
it('should handle very large arrays efficiently', () => {
|
||||
const largeArray = new Array(1000).fill('test');
|
||||
|
||||
expect(() => assertParamIsArray('testParam', largeArray, isString, mockNode)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user