Skip to main content
Glama

n8n-MCP

by 88-888
expression-format-validator.test.tsโ€ข12.9 kB
import { describe, it, expect } from 'vitest'; import { ExpressionFormatValidator } from '../../../src/services/expression-format-validator'; describe('ExpressionFormatValidator', () => { describe('validateAndFix', () => { const context = { nodeType: 'n8n-nodes-base.httpRequest', nodeName: 'HTTP Request', nodeId: 'test-id-1' }; describe('Simple string expressions', () => { it('should detect missing = prefix for expression', () => { const value = '{{ $env.API_KEY }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('missing-prefix'); expect(issue?.correctedValue).toBe('={{ $env.API_KEY }}'); expect(issue?.severity).toBe('error'); }); it('should accept expression with = prefix', () => { const value = '={{ $env.API_KEY }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'apiKey', context); expect(issue).toBeNull(); }); it('should detect mixed content without prefix', () => { const value = 'Bearer {{ $env.TOKEN }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('missing-prefix'); expect(issue?.correctedValue).toBe('=Bearer {{ $env.TOKEN }}'); }); it('should accept mixed content with prefix', () => { const value = '=Bearer {{ $env.TOKEN }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'authorization', context); expect(issue).toBeNull(); }); it('should ignore plain strings without expressions', () => { const value = 'https://api.example.com'; const issue = ExpressionFormatValidator.validateAndFix(value, 'url', context); expect(issue).toBeNull(); }); }); describe('Resource Locator fields', () => { const githubContext = { nodeType: 'n8n-nodes-base.github', nodeName: 'GitHub', nodeId: 'github-1' }; it('should detect expression in owner field needing resource locator', () => { const value = '{{ $vars.GITHUB_OWNER }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('needs-resource-locator'); expect(issue?.correctedValue).toEqual({ __rl: true, value: '={{ $vars.GITHUB_OWNER }}', mode: 'expression' }); expect(issue?.severity).toBe('error'); }); it('should accept resource locator with expression', () => { const value = { __rl: true, value: '={{ $vars.GITHUB_OWNER }}', mode: 'expression' }; const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext); expect(issue).toBeNull(); }); it('should detect missing prefix in resource locator value', () => { const value = { __rl: true, value: '{{ $vars.GITHUB_OWNER }}', mode: 'expression' }; const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('missing-prefix'); expect(issue?.correctedValue.value).toBe('={{ $vars.GITHUB_OWNER }}'); }); it('should warn if expression has prefix but should use RL format', () => { const value = '={{ $vars.GITHUB_OWNER }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'owner', githubContext); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('needs-resource-locator'); expect(issue?.severity).toBe('warning'); }); }); describe('Multiple expressions', () => { it('should detect multiple expressions without prefix', () => { const value = '{{ $json.first }} - {{ $json.last }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context); expect(issue).toBeTruthy(); expect(issue?.issueType).toBe('missing-prefix'); expect(issue?.correctedValue).toBe('={{ $json.first }} - {{ $json.last }}'); }); it('should accept multiple expressions with prefix', () => { const value = '={{ $json.first }} - {{ $json.last }}'; const issue = ExpressionFormatValidator.validateAndFix(value, 'fullName', context); expect(issue).toBeNull(); }); }); describe('Edge cases', () => { it('should handle null values', () => { const issue = ExpressionFormatValidator.validateAndFix(null, 'field', context); expect(issue).toBeNull(); }); it('should handle undefined values', () => { const issue = ExpressionFormatValidator.validateAndFix(undefined, 'field', context); expect(issue).toBeNull(); }); it('should handle empty strings', () => { const issue = ExpressionFormatValidator.validateAndFix('', 'field', context); expect(issue).toBeNull(); }); it('should handle numbers', () => { const issue = ExpressionFormatValidator.validateAndFix(42, 'field', context); expect(issue).toBeNull(); }); it('should handle booleans', () => { const issue = ExpressionFormatValidator.validateAndFix(true, 'field', context); expect(issue).toBeNull(); }); it('should handle arrays', () => { const issue = ExpressionFormatValidator.validateAndFix(['item1', 'item2'], 'field', context); expect(issue).toBeNull(); }); }); }); describe('validateNodeParameters', () => { const context = { nodeType: 'n8n-nodes-base.emailSend', nodeName: 'Send Email', nodeId: 'email-1' }; it('should validate all parameters recursively', () => { const parameters = { fromEmail: '{{ $env.SENDER_EMAIL }}', toEmail: 'user@example.com', subject: 'Test {{ $json.type }}', body: { html: '<p>Hello {{ $json.name }}</p>', text: 'Hello {{ $json.name }}' }, options: { replyTo: '={{ $env.REPLY_EMAIL }}' } }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); expect(issues).toHaveLength(4); expect(issues.map(i => i.fieldPath)).toContain('fromEmail'); expect(issues.map(i => i.fieldPath)).toContain('subject'); expect(issues.map(i => i.fieldPath)).toContain('body.html'); expect(issues.map(i => i.fieldPath)).toContain('body.text'); }); it('should handle arrays with expressions', () => { const parameters = { recipients: [ '{{ $json.email1 }}', 'static@example.com', '={{ $json.email2 }}' ] }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); expect(issues).toHaveLength(1); expect(issues[0].fieldPath).toBe('recipients[0]'); expect(issues[0].correctedValue).toBe('={{ $json.email1 }}'); }); it('should handle nested objects', () => { const parameters = { config: { database: { host: '{{ $env.DB_HOST }}', port: 5432, name: 'mydb' } } }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); expect(issues).toHaveLength(1); expect(issues[0].fieldPath).toBe('config.database.host'); }); it('should skip circular references', () => { const circular: any = { a: 1 }; circular.self = circular; const parameters = { normal: '{{ $json.value }}', circular }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); // Should only find the issue in 'normal', not crash on circular expect(issues).toHaveLength(1); expect(issues[0].fieldPath).toBe('normal'); }); it('should handle maximum recursion depth', () => { // Create a deeply nested object (105 levels deep, exceeding the limit of 100) let deepObject: any = { value: '{{ $json.data }}' }; let current = deepObject; for (let i = 0; i < 105; i++) { current.nested = { value: `{{ $json.level${i} }}` }; current = current.nested; } const parameters = { deep: deepObject }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); // Should find expression format issues up to the depth limit const depthWarning = issues.find(i => i.explanation.includes('Maximum recursion depth')); expect(depthWarning).toBeTruthy(); expect(depthWarning?.severity).toBe('warning'); // Should still find some expression format errors before hitting the limit const formatErrors = issues.filter(i => i.issueType === 'missing-prefix'); expect(formatErrors.length).toBeGreaterThan(0); expect(formatErrors.length).toBeLessThanOrEqual(100); // Should not exceed the depth limit }); }); describe('formatErrorMessage', () => { const context = { nodeType: 'n8n-nodes-base.github', nodeName: 'Create Issue', nodeId: 'github-1' }; it('should format error message for missing prefix', () => { const issue = { fieldPath: 'title', currentValue: '{{ $json.title }}', correctedValue: '={{ $json.title }}', issueType: 'missing-prefix' as const, explanation: "Expression missing required '=' prefix.", severity: 'error' as const }; const message = ExpressionFormatValidator.formatErrorMessage(issue, context); expect(message).toContain("Expression format error in node 'Create Issue'"); expect(message).toContain('Field \'title\''); expect(message).toContain('Current (incorrect):'); expect(message).toContain('"title": "{{ $json.title }}"'); expect(message).toContain('Fixed (correct):'); expect(message).toContain('"title": "={{ $json.title }}"'); }); it('should format error message for resource locator', () => { const issue = { fieldPath: 'owner', currentValue: '{{ $vars.OWNER }}', correctedValue: { __rl: true, value: '={{ $vars.OWNER }}', mode: 'expression' }, issueType: 'needs-resource-locator' as const, explanation: 'Field needs resource locator format.', severity: 'error' as const }; const message = ExpressionFormatValidator.formatErrorMessage(issue, context); expect(message).toContain("Expression format error in node 'Create Issue'"); expect(message).toContain('Current (incorrect):'); expect(message).toContain('"owner": "{{ $vars.OWNER }}"'); expect(message).toContain('Fixed (correct):'); expect(message).toContain('"__rl": true'); expect(message).toContain('"value": "={{ $vars.OWNER }}"'); expect(message).toContain('"mode": "expression"'); }); }); describe('Real-world examples', () => { it('should validate Email Send node example', () => { const context = { nodeType: 'n8n-nodes-base.emailSend', nodeName: 'Error Handler', nodeId: 'b9dd1cfd-ee66-4049-97e7-1af6d976a4e0' }; const parameters = { fromEmail: '{{ $env.ADMIN_EMAIL }}', toEmail: 'admin@company.com', subject: 'GitHub Issue Workflow Error - HIGH PRIORITY', options: {} }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); expect(issues).toHaveLength(1); expect(issues[0].fieldPath).toBe('fromEmail'); expect(issues[0].correctedValue).toBe('={{ $env.ADMIN_EMAIL }}'); }); it('should validate GitHub node example', () => { const context = { nodeType: 'n8n-nodes-base.github', nodeName: 'Send Welcome Comment', nodeId: '3c742ca1-af8f-4d80-a47e-e68fb1ced491' }; const parameters = { operation: 'createComment', owner: '{{ $vars.GITHUB_OWNER }}', repository: '{{ $vars.GITHUB_REPO }}', issueNumber: null, body: '๐Ÿ‘‹ Hi @{{ $(\'Extract Issue Data\').first().json.author }}!\n\nThank you for creating this issue.' }; const issues = ExpressionFormatValidator.validateNodeParameters(parameters, context); expect(issues.length).toBeGreaterThan(0); expect(issues.some(i => i.fieldPath === 'owner')).toBe(true); expect(issues.some(i => i.fieldPath === 'repository')).toBe(true); expect(issues.some(i => i.fieldPath === 'body')).toBe(true); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server