mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-24 13:25:26 +02:00
269 lines
6.0 KiB
TypeScript
269 lines
6.0 KiB
TypeScript
import {
|
|
safeParseWorkflowStructure,
|
|
parseWorkflowStructure,
|
|
WorkflowStructureValidationError,
|
|
} from '../src/workflow-structure-validation';
|
|
|
|
describe('workflow-structure-validation', () => {
|
|
const validWorkflow = {
|
|
nodes: [
|
|
{
|
|
id: 'node-1',
|
|
name: 'Start',
|
|
type: 'n8n-nodes-base.manualTrigger',
|
|
position: [0, 0] as [number, number],
|
|
parameters: {},
|
|
},
|
|
{
|
|
id: 'node-2',
|
|
name: 'Set',
|
|
type: 'n8n-nodes-base.set',
|
|
position: [200, 0] as [number, number],
|
|
parameters: {},
|
|
},
|
|
],
|
|
connections: {
|
|
Start: {
|
|
main: [[{ node: 'Set', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
};
|
|
|
|
test('accepts a structurally valid workflow', () => {
|
|
expect(safeParseWorkflowStructure(validWorkflow)).toEqual({
|
|
success: true,
|
|
data: validWorkflow,
|
|
});
|
|
});
|
|
|
|
test('accepts a valid workflow with empty connections', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
nodes: [validWorkflow.nodes[0]],
|
|
connections: {},
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
test('accepts a valid workflow with empty nodes array', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
nodes: [],
|
|
connections: {},
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
test('accepts null connection buckets (unused output slots)', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
connections: {
|
|
Start: {
|
|
main: [null, [{ node: 'Set', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
test('rejects nodes missing a required field', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
nodes: [{ ...validWorkflow.nodes[0], type: undefined }],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'invalid_type',
|
|
path: ['nodes', 0, 'type'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects empty string node name', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
nodes: [{ ...validWorkflow.nodes[0], name: '' }],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'too_small',
|
|
path: ['nodes', 0, 'name'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects positions with fewer than two coordinates', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
nodes: [{ ...validWorkflow.nodes[0], position: [0] }],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'too_small',
|
|
path: ['nodes', 0, 'position'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects positions with more than two coordinates', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
nodes: [{ ...validWorkflow.nodes[0], position: [0, 0, 50] }],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'too_big',
|
|
path: ['nodes', 0, 'position'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects connection with negative index', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
connections: {
|
|
Start: {
|
|
main: [[{ node: 'Set', type: 'main', index: -1 }]],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'too_small',
|
|
path: ['connections', 'Start', 'main', 0, 0, 'index'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects duplicate node names', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
nodes: [
|
|
validWorkflow.nodes[0],
|
|
{
|
|
...validWorkflow.nodes[1],
|
|
name: 'Start',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'duplicate_node_name',
|
|
path: ['nodes', 1, 'name'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects unknown connection sources', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
connections: {
|
|
Missing: {
|
|
main: [[{ node: 'Set', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'unknown_connection_source',
|
|
path: ['connections', 'Missing'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects unknown connection targets', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
...validWorkflow,
|
|
connections: {
|
|
Start: {
|
|
main: [[{ node: 'Missing', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'unknown_connection_target',
|
|
path: ['connections', 'Start', 'main', 0, 0, 'node'],
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('rejects empty nodes with non-empty connections', () => {
|
|
const result = safeParseWorkflowStructure({
|
|
nodes: [],
|
|
connections: {
|
|
Start: {
|
|
main: [[{ node: 'Set', type: 'main', index: 0 }]],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
|
|
expect(result.issues).toContainEqual(
|
|
expect.objectContaining({
|
|
code: 'unknown_connection_source',
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('throws a typed error for invalid workflows', () => {
|
|
expect(() =>
|
|
parseWorkflowStructure({
|
|
nodes: [{ name: 'Start', position: [0, 0], parameters: {} }],
|
|
connections: {},
|
|
}),
|
|
).toThrow(WorkflowStructureValidationError);
|
|
});
|
|
|
|
test('error description formats issue paths', () => {
|
|
let thrown: WorkflowStructureValidationError | undefined;
|
|
|
|
try {
|
|
parseWorkflowStructure({
|
|
nodes: [{ name: 'Start', position: [0, 0], parameters: {} }],
|
|
connections: {},
|
|
});
|
|
} catch (error) {
|
|
thrown = error as WorkflowStructureValidationError;
|
|
}
|
|
|
|
expect(thrown).toBeInstanceOf(WorkflowStructureValidationError);
|
|
expect(thrown?.description).toContain('nodes[0].type');
|
|
});
|
|
});
|