first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,177 @@
import type { IN8nHttpFullResponse, INodeExecutionData, JsonObject } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { handleError, ErrorMap } from '../../helpers/errorHandler';
const mockExecuteSingleFunctions = {
getNode: jest.fn(() => ({ name: 'MockNode' })),
getNodeParameter: jest.fn(),
} as any;
describe('handleError', () => {
let response: IN8nHttpFullResponse;
let data: INodeExecutionData[];
beforeEach(() => {
data = [{}] as INodeExecutionData[];
response = { statusCode: 200, body: {} } as IN8nHttpFullResponse;
});
test('should return data when no error occurs', async () => {
const result = await handleError.call(mockExecuteSingleFunctions, data, response);
expect(result).toBe(data);
});
test('should throw NodeApiError for container conflict', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 409;
response.body = { code: 'Conflict', message: 'Container already exists' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Container.Conflict.getMessage('container'),
description: ErrorMap.Container.Conflict.description,
}),
);
});
test('should throw NodeApiError for container not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 404;
response.body = { code: 'NotFound', message: 'Container not found' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Container.NotFound.getMessage('container'),
description: ErrorMap.Container.NotFound.description,
}),
);
});
test('should throw NodeApiError for item not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('item');
response.statusCode = 404;
response.body = { code: 'NotFound', message: 'Item not found' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: ErrorMap.Item.NotFound.getMessage('item'),
description: ErrorMap.Item.NotFound.description,
}),
);
});
test('should throw generic error if no specific mapping exists', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('container');
response.statusCode = 400;
response.body = { code: 'BadRequest', message: 'Invalid request' } as JsonObject;
await expect(handleError.call(mockExecuteSingleFunctions, data, response)).rejects.toThrow(
new NodeApiError(mockExecuteSingleFunctions.getNode(), response.body as JsonObject, {
message: 'BadRequest',
description: 'Invalid request',
}),
);
});
test('should handle error details correctly when match is successful', async () => {
const errorMessage = 'Message: {"Errors":["Error 1", "Error 2"]}';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
expect(errorDetails).toEqual(['Error 1', 'Error 2']);
});
test('should handle error when match does not return expected format', async () => {
const errorMessage = 'Message: Invalid format';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
expect(errorDetails).toEqual([]);
});
test('should throw NodeApiError with proper details if error details are present', async () => {
const errorMessage = 'Message: {"Errors":["Specific error occurred"]}';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
if (errorDetails && errorDetails.length > 0) {
await expect(
handleError.call(mockExecuteSingleFunctions, data, {
statusCode: 500,
body: { code: 'InternalServerError', message: errorMessage },
headers: {},
}),
).rejects.toThrow(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{
code: 'InternalServerError',
message: errorMessage,
} as JsonObject,
{
message: 'InternalServerError',
description: errorDetails.join('\n'),
},
),
);
}
});
test('should throw NodeApiError with fallback message if no details found', async () => {
const errorMessage = 'Message: {"Errors":[] }';
const match = errorMessage.match(/Message: ({.*?})/);
let errorDetails: string[] = [];
if (match?.[1]) {
try {
errorDetails = JSON.parse(match[1]).Errors;
} catch {}
}
if (errorDetails && errorDetails.length > 0) {
await expect(
handleError.call(mockExecuteSingleFunctions, data, {
statusCode: 500,
body: { code: 'InternalServerError', message: errorMessage },
headers: {},
}),
).rejects.toThrow(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{
code: 'InternalServerError',
message: errorMessage,
} as JsonObject,
{
message: 'InternalServerError',
description: 'Internal Server Error',
},
),
);
}
});
});
@@ -0,0 +1,360 @@
import { mock } from 'jest-mock-extended';
import type {
IDataObject,
IExecuteSingleFunctions,
IHttpRequestOptions,
INode,
INodeExecutionData,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError, OperationalError } from 'n8n-workflow';
const azureCosmosDbApiRequest = jest.fn();
jest.mock('../../transport', () => ({ azureCosmosDbApiRequest }));
import { ErrorMap } from '../../helpers/errorHandler';
import {
getPartitionKey,
simplifyData,
validateQueryParameters,
processJsonInput,
validatePartitionKey,
validateCustomProperties,
} from '../../helpers/utils';
interface RequestBodyWithParameters extends IDataObject {
parameters: Array<{ name: string; value: string }>;
}
const mockExecuteSingleFunctions = mock<IExecuteSingleFunctions>();
beforeEach(() => {
jest.resetAllMocks();
mockExecuteSingleFunctions.getNode.mockReturnValue({ name: 'MockNode' } as INode);
});
describe('getPartitionKey', () => {
test('should return partition key when found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
const result = await getPartitionKey.call(mockExecuteSingleFunctions);
expect(result).toBe('partitionKeyPath');
});
test('should throw NodeOperationError if partition key is not found', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue('containerName');
const mockApiResponse = {};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Partition key not found', {
description: 'Failed to determine the partition key for this collection',
}),
);
});
test('should throw NodeApiError for 404 error', async () => {
const containerName = 'containerName';
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(containerName);
const errorMessage = ErrorMap.Container.NotFound.getMessage(containerName);
const mockError = new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{},
{
httpCode: '404',
message: errorMessage,
description: ErrorMap.Container.NotFound.description,
},
);
azureCosmosDbApiRequest.mockRejectedValue(mockError);
await expect(getPartitionKey.call(mockExecuteSingleFunctions)).rejects.toThrowError(
new NodeApiError(
mockExecuteSingleFunctions.getNode(),
{},
{
message: errorMessage,
description: ErrorMap.Container.NotFound.description,
},
),
);
});
});
describe('validatePartitionKey', () => {
let requestOptions: any;
beforeEach(() => {
requestOptions = { body: {}, headers: {} };
azureCosmosDbApiRequest.mockClear();
});
test('should throw NodeOperationError when partition key is missing for "create" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({});
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
"Partition key not found in 'Item Contents'",
{
description:
"Partition key 'partitionKey' must be present and have a valid, non-empty value in 'Item Contents'.",
},
),
);
});
test('should throw NodeOperationError when partition key is missing for "update" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('update');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce({ partitionKey: '' });
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Partition key is missing or empty',
{
description: 'Ensure the "Partition Key" field has a valid, non-empty value.',
},
),
);
});
test('should throw NodeOperationError when partition key is missing for "get" operation', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('get');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce(undefined);
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Partition key is missing or empty',
{
description: 'Ensure the "Partition Key" field exists and has a valid, non-empty value.',
},
),
);
});
test('should throw NodeOperationError when invalid JSON is provided for customProperties', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('create');
mockExecuteSingleFunctions.getNodeParameter.mockReturnValueOnce('invalidJson');
const mockApiResponse = {
partitionKey: {
paths: ['/partitionKeyPath'],
},
};
azureCosmosDbApiRequest.mockResolvedValue(mockApiResponse);
await expect(
validatePartitionKey.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Invalid JSON format in "Item Contents"',
{
description: 'Ensure the "Item Contents" field contains a valid JSON object',
},
),
);
});
});
describe('simplifyData', () => {
test('should return the same data when "simple" parameter is false', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(false);
const items = [{ json: { foo: 'bar' } }] as INodeExecutionData[];
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
expect(result).toEqual(items);
});
test('should simplify the data when "simple" parameter is true', async () => {
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(true);
const items = [{ json: { _internalKey: 'value', foo: 'bar' } }] as INodeExecutionData[];
const result = await simplifyData.call(mockExecuteSingleFunctions, items, {} as any);
expect(result).toEqual([{ json: { foo: 'bar' } }]);
});
});
describe('validateQueryParameters', () => {
let requestOptions: IHttpRequestOptions;
beforeEach(() => {
requestOptions = { body: {}, headers: {} } as IHttpRequestOptions;
});
test('should throw NodeOperationError when parameter values do not match', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1')
.mockReturnValueOnce({ queryParameters: 'param1, param2' });
await expect(
validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(
mockExecuteSingleFunctions.getNode(),
'Empty parameter value provided',
{
description: 'Please provide non-empty values for the query parameters',
},
),
);
});
test('should successfully map parameters when they match', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1, $2')
.mockReturnValueOnce({ queryParameters: 'value1, value2' });
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
{ name: '@Param1', value: 'value1' },
{ name: '@Param2', value: 'value2' },
]);
} else {
throw new OperationalError('Expected result.body to contain a parameters array');
}
});
test('should correctly map parameters when query contains multiple dynamic values', async () => {
mockExecuteSingleFunctions.getNodeParameter
.mockReturnValueOnce('$1, $2, $3')
.mockReturnValueOnce({ queryParameters: 'firstValue, secondValue, thirdValue' });
const result = await validateQueryParameters.call(mockExecuteSingleFunctions, requestOptions);
if (result.body && (result.body as RequestBodyWithParameters).parameters) {
expect((result.body as RequestBodyWithParameters).parameters).toEqual([
{ name: '@Param1', value: 'firstValue' },
{ name: '@Param2', value: 'secondValue' },
{ name: '@Param3', value: 'thirdValue' },
]);
} else {
throw new OperationalError('Expected result.body to contain a parameters array');
}
});
test('should extract and map parameter names correctly using regex', async () => {
const query = '$1, $2, $3';
const queryParamsString = 'value1, value2, value3';
const parameterNames = query.replace(/\$(\d+)/g, '@param$1').match(/@\w+/g) ?? [];
const parameterValues = queryParamsString.split(',').map((val) => val.trim());
expect(parameterNames).toEqual(['@param1', '@param2', '@param3']);
expect(parameterValues).toEqual(['value1', 'value2', 'value3']);
});
});
describe('processJsonInput', () => {
test('should return parsed JSON when input is a valid JSON string', () => {
const result = processJsonInput('{"key": "value"}');
expect(result).toEqual({ key: 'value' });
});
test('should return input data when it is already an object', () => {
const result = processJsonInput({ key: 'value' });
expect(result).toEqual({ key: 'value' });
});
test('should throw OperationalError for invalid JSON string', () => {
const invalidJson = '{key: value}';
expect(() => processJsonInput(invalidJson)).toThrowError(
new OperationalError('Input must contain a valid JSON', { level: 'warning' }),
);
});
test('should throw OperationalError for invalid non-string and non-object input', () => {
const invalidInput = 123;
expect(() => processJsonInput(invalidInput, 'testInput')).toThrowError(
new OperationalError("Input 'testInput' must contain a valid JSON", { level: 'warning' }),
);
});
});
describe('validateCustomProperties', () => {
let requestOptions: any;
beforeEach(() => {
requestOptions = { body: {}, headers: {}, url: 'http://mock.url' };
});
test('should merge custom properties into requestOptions.body for valid input', async () => {
const validCustomProperties = { property1: 'value1', property2: 'value2' };
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(validCustomProperties);
const result = await validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions);
expect(result.body).toEqual({ property1: 'value1', property2: 'value2' });
});
test('should throw NodeOperationError when customProperties are empty, undefined, null, or contain only invalid values', async () => {
const emptyCustomProperties = {};
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(emptyCustomProperties);
await expect(
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
description: 'Ensure the "Item Contents" field contains at least one valid property.',
}),
);
const invalidValues = { property1: null, property2: '' };
mockExecuteSingleFunctions.getNodeParameter.mockReturnValue(invalidValues);
await expect(
validateCustomProperties.call(mockExecuteSingleFunctions, requestOptions),
).rejects.toThrowError(
new NodeOperationError(mockExecuteSingleFunctions.getNode(), 'Item contents are empty', {
description: 'Ensure the "Item Contents" field contains at least one valid property.',
}),
);
});
});