n8n/packages/@n8n/workflow-sdk/src/pin-data-utils.test.ts
2026-03-27 07:47:48 +00:00

268 lines
8.0 KiB
TypeScript

import type { INode, INodeExecutionData, IPinData } from 'n8n-workflow';
import { HTTP_REQUEST_NODE_TYPE } from 'n8n-workflow';
import {
needsPinData,
discoverOutputSchemaForNode,
inferSchemasFromRunData,
normalizePinData,
} from './pin-data-utils';
// Mock the generate-types module used by discoverOutputSchemaForNode
const mockDiscoverSchemasForNode = jest.fn();
const mockFindSchemaForOperation = jest.fn();
const mockGenerateJsonSchemaFromData = jest.fn();
jest.mock('./generate-types', () => ({
discoverSchemasForNode: (...args: unknown[]) => mockDiscoverSchemasForNode(...args) as unknown,
findSchemaForOperation: (...args: unknown[]) => mockFindSchemaForOperation(...args) as unknown,
generateJsonSchemaFromData: (...args: unknown[]) =>
mockGenerateJsonSchemaFromData(...args) as unknown,
}));
function createNode(overrides: Partial<INode> = {}): INode {
return {
id: 'node-1',
name: 'Test Node',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [0, 0] as [number, number],
parameters: {},
...overrides,
} as INode;
}
describe('needsPinData', () => {
it('returns false for a plain logic node without trigger check', () => {
const node = createNode({ type: 'n8n-nodes-base.set' });
expect(needsPinData(node)).toBe(false);
});
it('returns true when isTriggerNode callback returns true', () => {
const node = createNode({ type: 'n8n-nodes-base.webhook' });
expect(needsPinData(node, () => true)).toBe(true);
});
it('returns false when isTriggerNode callback returns false and no other criteria match', () => {
const node = createNode({ type: 'n8n-nodes-base.set' });
expect(needsPinData(node, () => false)).toBe(false);
});
it('returns true for nodes with credentials', () => {
const node = createNode({
type: 'n8n-nodes-base.slack',
credentials: { slackApi: { id: 'cred-1', name: 'My Slack' } },
});
expect(needsPinData(node)).toBe(true);
});
it('returns false for nodes with empty credentials object', () => {
const node = createNode({
type: 'n8n-nodes-base.slack',
credentials: {},
});
expect(needsPinData(node)).toBe(false);
});
it('returns true for HTTP Request nodes', () => {
const node = createNode({ type: HTTP_REQUEST_NODE_TYPE });
expect(needsPinData(node)).toBe(true);
});
it('returns true for HTTP Request nodes even when isTriggerNode returns false', () => {
const node = createNode({ type: HTTP_REQUEST_NODE_TYPE });
expect(needsPinData(node, () => false)).toBe(true);
});
it('returns true for credential nodes even when isTriggerNode is not provided', () => {
const node = createNode({
credentials: { someApi: { id: '1', name: 'cred' } },
});
expect(needsPinData(node)).toBe(true);
});
});
describe('discoverOutputSchemaForNode', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns undefined for empty node type', () => {
expect(discoverOutputSchemaForNode('', 1)).toBeUndefined();
});
it('returns undefined when no schemas are discovered', () => {
mockDiscoverSchemasForNode.mockReturnValue([]);
expect(discoverOutputSchemaForNode('n8n-nodes-base.slack', 1)).toBeUndefined();
});
it('uses findSchemaForOperation when resource/operation are provided', () => {
const schema = { type: 'object', properties: { id: { type: 'string' } } };
mockDiscoverSchemasForNode.mockReturnValue([
{ resource: 'message', operation: 'post', schema },
]);
mockFindSchemaForOperation.mockReturnValue({ schema });
const result = discoverOutputSchemaForNode('n8n-nodes-base.slack', 1, {
resource: 'message',
operation: 'post',
});
expect(result).toEqual(schema);
expect(mockFindSchemaForOperation).toHaveBeenCalledWith(expect.any(Array), 'message', 'post');
});
it('returns single schema when only one exists and no resource/operation', () => {
const schema = { type: 'object', properties: { val: { type: 'number' } } };
mockDiscoverSchemasForNode.mockReturnValue([{ schema }]);
const result = discoverOutputSchemaForNode('n8n-nodes-base.httpRequest', 4);
expect(result).toEqual(schema);
expect(mockFindSchemaForOperation).not.toHaveBeenCalled();
});
it('returns undefined when multiple schemas exist and no resource/operation', () => {
mockDiscoverSchemasForNode.mockReturnValue([
{ schema: { type: 'object' } },
{ schema: { type: 'object' } },
]);
const result = discoverOutputSchemaForNode('n8n-nodes-base.slack', 1);
expect(result).toBeUndefined();
});
it('returns undefined when findSchemaForOperation returns no match', () => {
mockDiscoverSchemasForNode.mockReturnValue([
{ resource: 'channel', operation: 'list', schema: {} },
]);
mockFindSchemaForOperation.mockReturnValue(undefined);
const result = discoverOutputSchemaForNode('n8n-nodes-base.slack', 1, {
resource: 'message',
operation: 'post',
});
expect(result).toBeUndefined();
});
});
describe('inferSchemasFromRunData', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns empty object for empty run data', () => {
expect(inferSchemasFromRunData({})).toEqual({});
});
it('skips nodes with no data items', () => {
const result = inferSchemasFromRunData({
EmptyNode: [],
});
expect(result).toEqual({});
});
it('skips nodes with empty json', () => {
mockGenerateJsonSchemaFromData.mockReturnValue({ type: 'object', properties: {} });
const result = inferSchemasFromRunData({
EmptyJsonNode: [{ json: {} } as INodeExecutionData],
});
expect(result).toEqual({});
expect(mockGenerateJsonSchemaFromData).not.toHaveBeenCalled();
});
it('infers schema from first item json', () => {
const schema = { type: 'object', properties: { id: { type: 'string' } } };
mockGenerateJsonSchemaFromData.mockReturnValue(schema);
const result = inferSchemasFromRunData({
SlackNode: [{ json: { id: '123', channel: 'general' } } as unknown as INodeExecutionData],
});
expect(result).toEqual({ SlackNode: schema });
expect(mockGenerateJsonSchemaFromData).toHaveBeenCalledWith({ id: '123', channel: 'general' });
});
it('processes multiple nodes', () => {
const schema1 = { type: 'object', properties: { a: { type: 'string' } } };
const schema2 = { type: 'object', properties: { b: { type: 'number' } } };
mockGenerateJsonSchemaFromData.mockReturnValueOnce(schema1).mockReturnValueOnce(schema2);
const result = inferSchemasFromRunData({
Node1: [{ json: { a: 'hello' } } as unknown as INodeExecutionData],
Node2: [{ json: { b: 42 } } as unknown as INodeExecutionData],
});
expect(result).toEqual({ Node1: schema1, Node2: schema2 });
});
});
describe('normalizePinData', () => {
it('passes through items already wrapped in json property', () => {
const pinData: IPinData = {
Trigger: [{ json: { id: '123', name: 'test' } }],
};
const result = normalizePinData(pinData);
expect(result).toEqual({
Trigger: [{ json: { id: '123', name: 'test' } }],
});
});
it('wraps flat objects in json property', () => {
const pinData = {
Trigger: [{ id: '123', name: 'test' }],
} as unknown as IPinData;
const result = normalizePinData(pinData);
expect(result).toEqual({
Trigger: [{ json: { id: '123', name: 'test' } }],
});
});
it('handles mixed items (some wrapped, some flat)', () => {
const pinData = {
Node1: [{ json: { wrapped: true } }, { flat: true }],
} as unknown as IPinData;
const result = normalizePinData(pinData);
expect(result).toEqual({
Node1: [{ json: { wrapped: true } }, { json: { flat: true } }],
});
});
it('handles empty pin data', () => {
expect(normalizePinData({})).toEqual({});
});
it('handles multiple nodes', () => {
const pinData: IPinData = {
Node1: [{ json: { a: 1 } }],
Node2: [{ json: { b: 2 } }],
};
const result = normalizePinData(pinData);
expect(result).toEqual({
Node1: [{ json: { a: 1 } }],
Node2: [{ json: { b: 2 } }],
});
});
it('wraps items where json is null', () => {
const pinData = {
Node1: [{ json: null }],
} as unknown as IPinData;
const result = normalizePinData(pinData);
expect(result).toEqual({
Node1: [{ json: { json: null } }],
});
});
});