mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 13:33:11 +00:00
* fix: critical telemetry improvements for data quality and security Fixed three critical issues in workflow mutation telemetry: 1. Fixed Inconsistent Sanitization (Security Critical) - Problem: 30% of workflows unsanitized, exposing credentials/tokens - Solution: Use robust WorkflowSanitizer.sanitizeWorkflowRaw() - Impact: 100% sanitization with 17 sensitive patterns redacted - Files: workflow-sanitizer.ts, mutation-tracker.ts 2. Enabled Validation Data Capture (Data Quality) - Problem: Zero validation metrics captured (all NULL) - Solution: Add pre/post mutation validation with WorkflowValidator - Impact: Measure mutation quality, track error resolution - Non-blocking validation that captures errors/warnings - Files: handlers-workflow-diff.ts 3. Improved Intent Capture (Data Quality) - Problem: 92.62% generic "Partial workflow update" intents - Solution: Enhanced docs + automatic intent inference - Impact: Meaningful intents auto-generated from operations - Files: n8n-update-partial-workflow.ts, handlers-workflow-diff.ts Expected Results: - 100% sanitization coverage (up from 70%) - 100% validation capture (up from 0%) - 50%+ meaningful intents (up from 7.33%) Version bumped to 2.22.17 🤖 Generated with [Claude Code](https://claude.com/claude-code) Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude <noreply@anthropic.com> * perf: implement validator instance caching to avoid redundant initialization - Add module-level cached WorkflowValidator instance - Create getValidator() helper to reuse validator across mutations - Update pre/post mutation validation to use cached instance - Avoids redundant NodeSimilarityService initialization on every mutation Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: restore backward-compatible sanitization with context preservation Fixed CI test failures by updating WorkflowSanitizer to use pattern-specific placeholders while maintaining backward compatibility: Changes: - Convert SENSITIVE_PATTERNS to PatternDefinition objects with specific placeholders - Update sanitizeString() to preserve context (Bearer prefix, URL paths) - Refactor sanitizeObject() to handle sensitive fields vs URL fields differently - Remove overly greedy field patterns that conflicted with token patterns Pattern-specific placeholders: - [REDACTED_URL_WITH_AUTH] for URLs with credentials - [REDACTED_TOKEN] for long tokens (32+ chars) - [REDACTED_APIKEY] for OpenAI-style keys - Bearer [REDACTED] for Bearer tokens (preserves "Bearer " prefix) - [REDACTED] for generic sensitive fields Test Results: - All 13 mutation-tracker tests passing - URL with auth: preserves path after credentials - Long tokens: properly detected and marked - OpenAI keys: correctly identified - Bearer tokens: prefix preserved - Sensitive field names: generic redaction for non-URL fields Fixes #421 CI failures Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: prevent double-redaction in workflow sanitizer Added safeguard to stop pattern matching once a placeholder is detected, preventing token patterns from matching text inside placeholders like [REDACTED_URL_WITH_AUTH]. Also expanded database URL pattern to match full URLs including port and path, and updated test expectations to match context-preserving sanitization. Fixes: - Database URLs now properly sanitized to [REDACTED_URL_WITH_AUTH] - Prevents [[REDACTED]] double-redaction issue - All 25 workflow-sanitizer tests passing - No regression in mutation-tracker tests Conceived by Romuald Członkowski - www.aiadvisors.pl/en --------- Co-authored-by: Claude <noreply@anthropic.com>
670 lines
22 KiB
TypeScript
670 lines
22 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { WorkflowSanitizer } from '../../../src/telemetry/workflow-sanitizer';
|
|
|
|
describe('WorkflowSanitizer', () => {
|
|
describe('sanitizeWorkflow', () => {
|
|
it('should remove API keys from parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'HTTP Request',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
url: 'https://api.example.com',
|
|
apiKey: 'sk-1234567890abcdef1234567890abcdef',
|
|
headers: {
|
|
'Authorization': 'Bearer sk-1234567890abcdef1234567890abcdef'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].parameters.apiKey).toBe('[REDACTED]');
|
|
expect(sanitized.nodes[0].parameters.headers.Authorization).toBe('[REDACTED]');
|
|
});
|
|
|
|
it('should sanitize webhook URLs but keep structure', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [100, 100],
|
|
parameters: {
|
|
path: 'my-webhook',
|
|
webhookUrl: 'https://n8n.example.com/webhook/abc-def-ghi',
|
|
method: 'POST'
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].parameters.webhookUrl).toBe('https://[webhook-url]');
|
|
expect(sanitized.nodes[0].parameters.method).toBe('POST'); // Method should remain
|
|
expect(sanitized.nodes[0].parameters.path).toBe('my-webhook'); // Path should remain
|
|
});
|
|
|
|
it('should remove credentials entirely', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Slack',
|
|
type: 'n8n-nodes-base.slack',
|
|
position: [100, 100],
|
|
parameters: {
|
|
channel: 'general',
|
|
text: 'Hello World'
|
|
},
|
|
credentials: {
|
|
slackApi: {
|
|
id: 'cred-123',
|
|
name: 'My Slack'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].credentials).toBeUndefined();
|
|
expect(sanitized.nodes[0].parameters.channel).toBe('general'); // Channel should remain
|
|
expect(sanitized.nodes[0].parameters.text).toBe('Hello World'); // Text should remain
|
|
});
|
|
|
|
it('should sanitize URLs in parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'HTTP Request',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
url: 'https://api.example.com/endpoint',
|
|
endpoint: 'https://another.example.com/api',
|
|
baseUrl: 'https://base.example.com'
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].parameters.url).toBe('https://[domain]/endpoint');
|
|
expect(sanitized.nodes[0].parameters.endpoint).toBe('https://[domain]/api');
|
|
expect(sanitized.nodes[0].parameters.baseUrl).toBe('https://[domain]');
|
|
});
|
|
|
|
it('should calculate workflow metrics correctly', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'HTTP Request',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [200, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Slack',
|
|
type: 'n8n-nodes-base.slack',
|
|
position: [300, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {
|
|
'1': {
|
|
main: [[{ node: '2', type: 'main', index: 0 }]]
|
|
},
|
|
'2': {
|
|
main: [[{ node: '3', type: 'main', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodeCount).toBe(3);
|
|
expect(sanitized.nodeTypes).toContain('n8n-nodes-base.webhook');
|
|
expect(sanitized.nodeTypes).toContain('n8n-nodes-base.httpRequest');
|
|
expect(sanitized.nodeTypes).toContain('n8n-nodes-base.slack');
|
|
expect(sanitized.hasTrigger).toBe(true);
|
|
expect(sanitized.hasWebhook).toBe(true);
|
|
expect(sanitized.complexity).toBe('simple');
|
|
});
|
|
|
|
it('should calculate complexity based on node count', () => {
|
|
const createWorkflow = (nodeCount: number) => ({
|
|
nodes: Array.from({ length: nodeCount }, (_, i) => ({
|
|
id: String(i),
|
|
name: `Node ${i}`,
|
|
type: 'n8n-nodes-base.function',
|
|
position: [i * 100, 100],
|
|
parameters: {}
|
|
})),
|
|
connections: {}
|
|
});
|
|
|
|
const simple = WorkflowSanitizer.sanitizeWorkflow(createWorkflow(5));
|
|
expect(simple.complexity).toBe('simple');
|
|
|
|
const medium = WorkflowSanitizer.sanitizeWorkflow(createWorkflow(15));
|
|
expect(medium.complexity).toBe('medium');
|
|
|
|
const complex = WorkflowSanitizer.sanitizeWorkflow(createWorkflow(25));
|
|
expect(complex.complexity).toBe('complex');
|
|
});
|
|
|
|
it('should generate consistent workflow hash', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
position: [100, 100],
|
|
parameters: { path: 'test' }
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const hash1 = WorkflowSanitizer.generateWorkflowHash(workflow);
|
|
const hash2 = WorkflowSanitizer.generateWorkflowHash(workflow);
|
|
|
|
expect(hash1).toBe(hash2);
|
|
expect(hash1).toMatch(/^[a-f0-9]{16}$/);
|
|
});
|
|
|
|
it('should sanitize nested objects in parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Complex Node',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
options: {
|
|
headers: {
|
|
'X-API-Key': 'secret-key-1234567890abcdef',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: {
|
|
data: 'some data',
|
|
token: 'another-secret-token-xyz123'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].parameters.options.headers['X-API-Key']).toBe('[REDACTED]');
|
|
expect(sanitized.nodes[0].parameters.options.headers['Content-Type']).toBe('application/json');
|
|
expect(sanitized.nodes[0].parameters.options.body.data).toBe('some data');
|
|
expect(sanitized.nodes[0].parameters.options.body.token).toBe('[REDACTED]');
|
|
});
|
|
|
|
it('should preserve connections structure', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Node 1',
|
|
type: 'n8n-nodes-base.start',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'Node 2',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [200, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {
|
|
'1': {
|
|
main: [[{ node: '2', type: 'main', index: 0 }]],
|
|
error: [[{ node: '2', type: 'error', index: 0 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.connections).toEqual({
|
|
'1': {
|
|
main: [[{ node: '2', type: 'main', index: 0 }]],
|
|
error: [[{ node: '2', type: 'error', index: 0 }]]
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should remove sensitive workflow metadata', () => {
|
|
const workflow = {
|
|
id: 'workflow-123',
|
|
name: 'My Workflow',
|
|
nodes: [],
|
|
connections: {},
|
|
settings: {
|
|
errorWorkflow: 'error-workflow-id',
|
|
timezone: 'America/New_York'
|
|
},
|
|
staticData: { some: 'data' },
|
|
pinData: { node1: 'pinned' },
|
|
credentials: { slack: 'cred-123' },
|
|
sharedWorkflows: ['user-456'],
|
|
ownedBy: 'user-123',
|
|
createdBy: 'user-123',
|
|
updatedBy: 'user-456'
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
// Verify that sensitive workflow-level properties are not in the sanitized output
|
|
// The sanitized workflow should only have specific fields as defined in SanitizedWorkflow interface
|
|
expect(sanitized.nodes).toEqual([]);
|
|
expect(sanitized.connections).toEqual({});
|
|
expect(sanitized.nodeCount).toBe(0);
|
|
expect(sanitized.nodeTypes).toEqual([]);
|
|
|
|
// Verify these fields don't exist in the sanitized output
|
|
const sanitizedAsAny = sanitized as any;
|
|
expect(sanitizedAsAny.settings).toBeUndefined();
|
|
expect(sanitizedAsAny.staticData).toBeUndefined();
|
|
expect(sanitizedAsAny.pinData).toBeUndefined();
|
|
expect(sanitizedAsAny.credentials).toBeUndefined();
|
|
expect(sanitizedAsAny.sharedWorkflows).toBeUndefined();
|
|
expect(sanitizedAsAny.ownedBy).toBeUndefined();
|
|
expect(sanitizedAsAny.createdBy).toBeUndefined();
|
|
expect(sanitizedAsAny.updatedBy).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('edge cases and error handling', () => {
|
|
it('should handle null or undefined workflow', () => {
|
|
// The actual implementation will throw because JSON.parse(JSON.stringify(null)) is valid but creates issues
|
|
expect(() => WorkflowSanitizer.sanitizeWorkflow(null as any)).toThrow();
|
|
expect(() => WorkflowSanitizer.sanitizeWorkflow(undefined as any)).toThrow();
|
|
});
|
|
|
|
it('should handle workflow without nodes', () => {
|
|
const workflow = {
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodeCount).toBe(0);
|
|
expect(sanitized.nodeTypes).toEqual([]);
|
|
expect(sanitized.nodes).toEqual([]);
|
|
expect(sanitized.hasTrigger).toBe(false);
|
|
expect(sanitized.hasWebhook).toBe(false);
|
|
});
|
|
|
|
it('should handle workflow without connections', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Test Node',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}
|
|
]
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.connections).toEqual({});
|
|
expect(sanitized.nodeCount).toBe(1);
|
|
});
|
|
|
|
it('should handle malformed nodes array', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '2',
|
|
name: 'Valid Node',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
// Should handle workflow gracefully
|
|
expect(sanitized.nodeCount).toBe(1);
|
|
expect(sanitized.nodes.length).toBe(1);
|
|
});
|
|
|
|
it('should handle deeply nested objects in parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Deep Node',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
level1: {
|
|
level2: {
|
|
level3: {
|
|
level4: {
|
|
level5: {
|
|
secret: 'deep-secret-key-1234567890abcdef',
|
|
safe: 'safe-value'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes[0].parameters.level1.level2.level3.level4.level5.secret).toBe('[REDACTED]');
|
|
expect(sanitized.nodes[0].parameters.level1.level2.level3.level4.level5.safe).toBe('safe-value');
|
|
});
|
|
|
|
it('should handle circular references gracefully', () => {
|
|
const workflow: any = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Circular Node',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [100, 100],
|
|
parameters: {}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
// Create circular reference
|
|
workflow.nodes[0].parameters.selfRef = workflow.nodes[0];
|
|
|
|
// JSON.stringify throws on circular references, so this should throw
|
|
expect(() => WorkflowSanitizer.sanitizeWorkflow(workflow)).toThrow();
|
|
});
|
|
|
|
it('should handle extremely large workflows', () => {
|
|
const largeWorkflow = {
|
|
nodes: Array.from({ length: 1000 }, (_, i) => ({
|
|
id: String(i),
|
|
name: `Node ${i}`,
|
|
type: 'n8n-nodes-base.function',
|
|
position: [i * 10, 100],
|
|
parameters: {
|
|
code: `// Node ${i} code here`.repeat(100) // Large parameter
|
|
}
|
|
})),
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(largeWorkflow);
|
|
|
|
expect(sanitized.nodeCount).toBe(1000);
|
|
expect(sanitized.complexity).toBe('complex');
|
|
});
|
|
|
|
it('should handle various sensitive data patterns', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Sensitive Node',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
// Different patterns of sensitive data
|
|
api_key: 'sk-1234567890abcdef1234567890abcdef',
|
|
accessToken: 'ghp_abcdefghijklmnopqrstuvwxyz123456',
|
|
secret_token: 'secret-123-abc-def',
|
|
authKey: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
clientSecret: 'abc123def456ghi789',
|
|
webhookUrl: 'https://hooks.example.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
databaseUrl: 'postgres://user:password@localhost:5432/db',
|
|
connectionString: 'Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;',
|
|
// Safe values that should remain
|
|
timeout: 5000,
|
|
method: 'POST',
|
|
retries: 3,
|
|
name: 'My API Call'
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
const params = sanitized.nodes[0].parameters;
|
|
expect(params.api_key).toBe('[REDACTED]');
|
|
expect(params.accessToken).toBe('[REDACTED]');
|
|
expect(params.secret_token).toBe('[REDACTED]');
|
|
expect(params.authKey).toBe('[REDACTED]');
|
|
expect(params.clientSecret).toBe('[REDACTED]');
|
|
expect(params.webhookUrl).toBe('https://hooks.example.com/services/T00000000/B00000000/[REDACTED]');
|
|
expect(params.databaseUrl).toBe('[REDACTED_URL_WITH_AUTH]');
|
|
expect(params.connectionString).toBe('[REDACTED]');
|
|
|
|
// Safe values should remain
|
|
expect(params.timeout).toBe(5000);
|
|
expect(params.method).toBe('POST');
|
|
expect(params.retries).toBe(3);
|
|
expect(params.name).toBe('My API Call');
|
|
});
|
|
|
|
it('should handle arrays in parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Array Node',
|
|
type: 'n8n-nodes-base.httpRequest',
|
|
position: [100, 100],
|
|
parameters: {
|
|
headers: [
|
|
{ name: 'Authorization', value: 'Bearer secret-token-123456789' },
|
|
{ name: 'Content-Type', value: 'application/json' },
|
|
{ name: 'X-API-Key', value: 'api-key-abcdefghijklmnopqrstuvwxyz' }
|
|
],
|
|
methods: ['GET', 'POST']
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
const headers = sanitized.nodes[0].parameters.headers;
|
|
expect(headers[0].value).toBe('Bearer [REDACTED]'); // Authorization (Bearer prefix preserved)
|
|
expect(headers[1].value).toBe('application/json'); // Content-Type (safe)
|
|
expect(headers[2].value).toBe('[REDACTED_TOKEN]'); // X-API-Key (32+ chars)
|
|
expect(sanitized.nodes[0].parameters.methods).toEqual(['GET', 'POST']); // Array should remain
|
|
});
|
|
|
|
it('should handle mixed data types in parameters', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Mixed Node',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [100, 100],
|
|
parameters: {
|
|
numberValue: 42,
|
|
booleanValue: true,
|
|
stringValue: 'safe string',
|
|
nullValue: null,
|
|
undefinedValue: undefined,
|
|
dateValue: new Date('2024-01-01'),
|
|
arrayValue: [1, 2, 3],
|
|
nestedObject: {
|
|
secret: 'secret-key-12345678',
|
|
safe: 'safe-value'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
const params = sanitized.nodes[0].parameters;
|
|
expect(params.numberValue).toBe(42);
|
|
expect(params.booleanValue).toBe(true);
|
|
expect(params.stringValue).toBe('safe string');
|
|
expect(params.nullValue).toBeNull();
|
|
expect(params.undefinedValue).toBeUndefined();
|
|
expect(params.arrayValue).toEqual([1, 2, 3]);
|
|
expect(params.nestedObject.secret).toBe('[REDACTED]');
|
|
expect(params.nestedObject.safe).toBe('safe-value');
|
|
});
|
|
|
|
it('should handle missing node properties gracefully', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{ id: '3', name: 'Complete', type: 'n8n-nodes-base.function' } // Missing position but has required fields
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodes).toBeDefined();
|
|
expect(sanitized.nodeCount).toBe(1);
|
|
});
|
|
|
|
it('should handle complex connection structures', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{ id: '1', name: 'Start', type: 'n8n-nodes-base.start', position: [0, 0], parameters: {} },
|
|
{ id: '2', name: 'Branch', type: 'n8n-nodes-base.if', position: [100, 0], parameters: {} },
|
|
{ id: '3', name: 'Path A', type: 'n8n-nodes-base.function', position: [200, 0], parameters: {} },
|
|
{ id: '4', name: 'Path B', type: 'n8n-nodes-base.function', position: [200, 100], parameters: {} },
|
|
{ id: '5', name: 'Merge', type: 'n8n-nodes-base.merge', position: [300, 50], parameters: {} }
|
|
],
|
|
connections: {
|
|
'1': {
|
|
main: [[{ node: '2', type: 'main', index: 0 }]]
|
|
},
|
|
'2': {
|
|
main: [
|
|
[{ node: '3', type: 'main', index: 0 }],
|
|
[{ node: '4', type: 'main', index: 0 }]
|
|
]
|
|
},
|
|
'3': {
|
|
main: [[{ node: '5', type: 'main', index: 0 }]]
|
|
},
|
|
'4': {
|
|
main: [[{ node: '5', type: 'main', index: 1 }]]
|
|
}
|
|
}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.connections).toEqual(workflow.connections);
|
|
expect(sanitized.nodeCount).toBe(5);
|
|
expect(sanitized.complexity).toBe('simple'); // 5 nodes = simple
|
|
});
|
|
|
|
it('should generate different hashes for different workflows', () => {
|
|
const workflow1 = {
|
|
nodes: [{ id: '1', name: 'Node1', type: 'type1', position: [0, 0], parameters: {} }],
|
|
connections: {}
|
|
};
|
|
|
|
const workflow2 = {
|
|
nodes: [{ id: '1', name: 'Node2', type: 'type2', position: [0, 0], parameters: {} }],
|
|
connections: {}
|
|
};
|
|
|
|
const hash1 = WorkflowSanitizer.generateWorkflowHash(workflow1);
|
|
const hash2 = WorkflowSanitizer.generateWorkflowHash(workflow2);
|
|
|
|
expect(hash1).not.toBe(hash2);
|
|
expect(hash1).toMatch(/^[a-f0-9]{16}$/);
|
|
expect(hash2).toMatch(/^[a-f0-9]{16}$/);
|
|
});
|
|
|
|
it('should handle workflow with only trigger nodes', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{ id: '1', name: 'Cron', type: 'n8n-nodes-base.cron', position: [0, 0], parameters: {} },
|
|
{ id: '2', name: 'Webhook', type: 'n8n-nodes-base.webhook', position: [100, 0], parameters: {} }
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.hasTrigger).toBe(true);
|
|
expect(sanitized.hasWebhook).toBe(true);
|
|
expect(sanitized.nodeTypes).toContain('n8n-nodes-base.cron');
|
|
expect(sanitized.nodeTypes).toContain('n8n-nodes-base.webhook');
|
|
});
|
|
|
|
it('should handle workflow with special characters in node names and types', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: '1',
|
|
name: 'Node with émojis 🚀 and specíal chars',
|
|
type: 'n8n-nodes-base.function',
|
|
position: [0, 0],
|
|
parameters: {
|
|
message: 'Test with émojis 🎉 and URLs https://example.com'
|
|
}
|
|
}
|
|
],
|
|
connections: {}
|
|
};
|
|
|
|
const sanitized = WorkflowSanitizer.sanitizeWorkflow(workflow);
|
|
|
|
expect(sanitized.nodeCount).toBe(1);
|
|
expect(sanitized.nodes[0].name).toBe('Node with émojis 🚀 and specíal chars');
|
|
});
|
|
});
|
|
}); |