import get from 'lodash/get'; import { constructExecutionMetaData } from 'n8n-core'; import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow'; import type { SetNodeOptions } from '../../v2/helpers/interfaces'; import { composeReturnItem, parseJsonParameter, validateEntry, resolveRawData, } from '../../v2/helpers/utils'; export const node: INode = { id: '11', name: 'Edit Fields', type: 'n8n-nodes-base.set', typeVersion: 3, position: [42, 42], parameters: { mode: 'manual', fields: { values: [], }, include: 'none', options: {}, }, }; export const createMockExecuteFunction = (nodeParameters: IDataObject) => { const fakeExecuteFunction = { getNodeParameter( parameterName: string, _itemIndex: number, fallbackValue?: IDataObject, options?: IGetNodeParameterOptions, ) { const parameter = options?.extractValue ? `${parameterName}.value` : parameterName; return get(nodeParameters, parameter, fallbackValue); }, getNode() { return node; }, helpers: { constructExecutionMetaData }, continueOnFail: () => false, } as unknown as IExecuteFunctions; return fakeExecuteFunction; }; describe('test Set2, composeReturnItem', () => { it('should compose return item including no other fields', () => { const fakeExecuteFunction = createMockExecuteFunction({}); const inputItem = { json: { input1: 'value1', input2: 2, input3: [1, 2, 3], }, pairedItem: { item: 0, input: undefined, }, }; const newData = { num1: 55, str1: '42', arr1: ['foo', 'bar'], obj: { key: 'value', }, }; const options: SetNodeOptions = { include: 'none', }; const result = composeReturnItem.call(fakeExecuteFunction, 0, inputItem, newData, options, 3.4); expect(result).toEqual({ json: { num1: 55, str1: '42', arr1: ['foo', 'bar'], obj: { key: 'value', }, }, pairedItem: { item: 0, }, }); }); it('should compose return item including selected fields', () => { const fakeExecuteFunction = createMockExecuteFunction({ includeFields: 'input1, input2' }); const inputItem = { json: { input1: 'value1', input2: 2, input3: [1, 2, 3], }, pairedItem: { item: 0, input: undefined, }, }; const newData = { num1: 55, str1: '42', arr1: ['foo', 'bar'], obj: { key: 'value', }, }; const options: SetNodeOptions = { include: 'selected', }; const result = composeReturnItem.call(fakeExecuteFunction, 0, inputItem, newData, options, 3.4); expect(result).toEqual({ json: { num1: 55, str1: '42', arr1: ['foo', 'bar'], input1: 'value1', input2: 2, obj: { key: 'value', }, }, pairedItem: { item: 0, }, }); }); it('should include binary when expected in version <3.4', () => { const fakeExecuteFunction = createMockExecuteFunction({}); const inputItem = { json: { input1: 'value1', input2: 2, input3: [1, 2, 3], }, pairedItem: { item: 0, input: undefined, }, binary: { data: { data: 'content', mimeType: 'image/jpg', }, }, }; const newData = { num1: 55, str1: '42', arr1: ['foo', 'bar'], obj: { key: 'value', }, }; const resultWithIncludeBinary = composeReturnItem.call( fakeExecuteFunction, 0, inputItem, newData, { include: 'all', includeBinary: true, }, 3.3, ); expect(resultWithIncludeBinary.binary).toEqual(inputItem.binary); const resultWithoutIncludeBinary = composeReturnItem.call( fakeExecuteFunction, 0, inputItem, newData, { include: 'all', }, 3.3, ); expect(resultWithoutIncludeBinary.binary).toBeUndefined(); }); it('should include binary when expected in version >=3.4', () => { const fakeExecuteFunction = createMockExecuteFunction({}); const inputItem = { json: { input1: 'value1', input2: 2, input3: [1, 2, 3], }, pairedItem: { item: 0, input: undefined, }, binary: { data: { data: 'content', mimeType: 'image/jpg', }, }, }; const newData = { num1: 55, str1: '42', arr1: ['foo', 'bar'], obj: { key: 'value', }, }; const resultWithStripBinary = composeReturnItem.call( fakeExecuteFunction, 0, inputItem, newData, { include: 'all', stripBinary: true, }, 3.4, ); expect(resultWithStripBinary.binary).toBeUndefined(); const resultWithoutStripBinary = composeReturnItem.call( fakeExecuteFunction, 0, inputItem, newData, { include: 'all', }, 3.4, ); expect(resultWithoutStripBinary.binary).toEqual(inputItem.binary); }); }); describe('test Set2, parseJsonParameter', () => { it('should parse valid JSON string', () => { const result = parseJsonParameter('{"foo": "bar"}', node, 0, 'test'); expect(result).toEqual({ foo: 'bar', }); }); it('should tolerate single quotes in string', () => { const result = parseJsonParameter("{'foo': 'bar'}", node, 0, 'test'); expect(result).toEqual({ foo: 'bar', }); }); it('should tolerate unquoted keys', () => { const result = parseJsonParameter("{foo: 'bar'}", node, 0, 'test'); expect(result).toEqual({ foo: 'bar', }); }); it('should tolerate trailing comma', () => { const result = parseJsonParameter('{"foo": "bar"},', node, 0, 'test'); expect(result).toEqual({ foo: 'bar', }); }); it('should tolerate trailing commas in objects', () => { const result = parseJsonParameter("{foo: 'bar', baz: {'foo': 'bar',}, }", node, 0, 'test'); expect(result).toEqual({ foo: 'bar', baz: { foo: 'bar', }, }); }); }); describe('test Set2, validateEntry', () => { it('should convert number to string', () => { const result = validateEntry('foo', 'string', 42 as unknown as string, node, 0); expect(result).toEqual({ name: 'foo', value: '42', }); }); it('should convert array to string', () => { const result = validateEntry('foo', 'string', [1, 2, 3] as unknown as string, node, 0); expect(result).toEqual({ name: 'foo', value: '[1,2,3]', }); }); it('should convert object to string', () => { const result = validateEntry('foo', 'string', { foo: 'bar' } as unknown as string, node, 0); expect(result).toEqual({ name: 'foo', value: '{"foo":"bar"}', }); }); it('should convert boolean to string', () => { const result = validateEntry('foo', 'string', true as unknown as string, node, 0); expect(result).toEqual({ name: 'foo', value: 'true', }); }); it('should convert undefined to string', () => { const result = validateEntry('foo', 'string', undefined as unknown as string, node, 0); expect(result).toEqual({ name: 'foo', value: 'undefined', }); }); }); describe('test Set2, resolveRawData', () => { const createMockExecuteFunctionForResolve = (returnValue: any) => { return { evaluateExpression: jest.fn().mockReturnValue(returnValue), } as unknown as IExecuteFunctions; }; it('should handle strings with special replacement patterns like $&', () => { const mockFunction = createMockExecuteFunctionForResolve('hello world $&'); const input = '{{ "hello world $&" }}'; const result = resolveRawData.call(mockFunction, input, 0); // The result should contain the literal $& and not have it replaced expect(result).toBe('hello world $&'); expect(mockFunction.evaluateExpression).toHaveBeenCalledWith('{{ "hello world $&" }}', 0); }); it('should handle strings with $` pattern', () => { const mockFunction = createMockExecuteFunctionForResolve('hello world $`'); const input = '{{ "hello world $`" }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('hello world $`'); }); it("should handle strings with $' pattern", () => { const mockFunction = createMockExecuteFunctionForResolve("hello world $'"); const input = '{{ "hello world $\'" }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe("hello world $'"); }); it('should handle strings with $n pattern', () => { const mockFunction = createMockExecuteFunctionForResolve('hello world $1 $2'); const input = '{{ "hello world $1 $2" }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('hello world $1 $2'); }); it('should handle JSON objects with special patterns', () => { const mockFunction = createMockExecuteFunctionForResolve({ message: 'hello world $&' }); const input = '{{ {"message": "hello world $&"} }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('{"message":"hello world $&"}'); }); it('should handle multiple expressions with special patterns', () => { const mockFunction = { evaluateExpression: jest.fn().mockReturnValueOnce('hello $&').mockReturnValueOnce('world $`'), } as unknown as IExecuteFunctions; const input = 'start {{ expr1 }} middle {{ expr2 }} end'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('start hello $& middle world $` end'); }); it('should handle regular strings without special patterns', () => { const mockFunction = createMockExecuteFunctionForResolve('hello world'); const input = '{{ "hello world" }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('hello world'); }); it('should handle objects and stringify them', () => { const mockFunction = createMockExecuteFunctionForResolve({ message: 'hello world', count: 42 }); const input = '{{ someExpression }}'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('{"message":"hello world","count":42}'); }); it('should return raw data when no resolvables are found', () => { const mockFunction = createMockExecuteFunctionForResolve('unused'); const input = 'hello world without expressions'; const result = resolveRawData.call(mockFunction, input, 0); expect(result).toBe('hello world without expressions'); expect(mockFunction.evaluateExpression).not.toHaveBeenCalled(); }); });