Skip to main content
Glama

n8n-MCP

by 88-888
event-validator.test.tsβ€’17.7 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { z } from 'zod'; import { TelemetryEventValidator, telemetryEventSchema, workflowTelemetrySchema } from '../../../src/telemetry/event-validator'; import { TelemetryEvent, WorkflowTelemetry } from '../../../src/telemetry/telemetry-types'; // Mock logger to avoid console output in tests vi.mock('../../../src/utils/logger', () => ({ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), } })); describe('TelemetryEventValidator', () => { let validator: TelemetryEventValidator; beforeEach(() => { validator = new TelemetryEventValidator(); vi.clearAllMocks(); }); describe('validateEvent()', () => { it('should validate a basic valid event', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'tool_used', properties: { tool: 'httpRequest', success: true, duration: 500 } }; const result = validator.validateEvent(event); expect(result).toEqual(event); }); it('should validate event with specific schema for tool_used', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'tool_used', properties: { tool: 'httpRequest', success: true, duration: 500 } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.tool).toBe('httpRequest'); expect(result?.properties.success).toBe(true); expect(result?.properties.duration).toBe(500); }); it('should validate search_query event with specific schema', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'search_query', properties: { query: 'test query', resultsFound: 5, searchType: 'nodes', hasResults: true, isZeroResults: false } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.query).toBe('test query'); expect(result?.properties.resultsFound).toBe(5); expect(result?.properties.hasResults).toBe(true); }); it('should validate performance_metric event with specific schema', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'performance_metric', properties: { operation: 'database_query', duration: 1500, isSlow: true, isVerySlow: false, metadata: { table: 'nodes' } } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.operation).toBe('database_query'); expect(result?.properties.duration).toBe(1500); expect(result?.properties.isSlow).toBe(true); }); it('should sanitize sensitive data from properties', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'generic_event', properties: { description: 'Visit https://example.com/secret and user@example.com with key abcdef123456789012345678901234567890', apiKey: 'super-secret-key-12345678901234567890', normalProp: 'normal value' } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.description).toBe('Visit [URL] and [EMAIL] with key [KEY]'); expect(result?.properties.normalProp).toBe('normal value'); expect(result?.properties).not.toHaveProperty('apiKey'); // Should be filtered out }); it('should handle nested object sanitization with depth limit', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'nested_event', properties: { nested: { level1: { level2: { level3: { level4: 'should be truncated', apiKey: 'secret123', description: 'Visit https://example.com' }, description: 'Visit https://another.com' } } } } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.nested.level1.level2.level3).toBe('[NESTED]'); expect(result?.properties.nested.level1.level2.description).toBe('Visit [URL]'); }); it('should handle array sanitization with size limit', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'array_event', properties: { items: Array.from({ length: 15 }, (_, i) => ({ id: i, description: 'Visit https://example.com', value: `item-${i}` })) } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(Array.isArray(result?.properties.items)).toBe(true); expect(result?.properties.items.length).toBe(10); // Should be limited to 10 }); it('should reject events with invalid user_id', () => { const event: TelemetryEvent = { user_id: '', // Empty string event: 'test_event', properties: {} }; const result = validator.validateEvent(event); expect(result).toBeNull(); }); it('should reject events with invalid event name', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'invalid-event-name!@#', // Invalid characters properties: {} }; const result = validator.validateEvent(event); expect(result).toBeNull(); }); it('should reject tool_used event with invalid properties', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'tool_used', properties: { tool: 'test', success: 'not-a-boolean', // Should be boolean duration: -1 // Should be positive } }; const result = validator.validateEvent(event); expect(result).toBeNull(); }); it('should filter out sensitive keys from properties', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'sensitive_event', properties: { password: 'secret123', token: 'bearer-token', apikey: 'api-key-value', secret: 'secret-value', credential: 'cred-value', auth: 'auth-header', url: 'https://example.com', endpoint: 'api.example.com', host: 'localhost', database: 'prod-db', normalProp: 'safe-value', count: 42, enabled: true } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties).not.toHaveProperty('password'); expect(result?.properties).not.toHaveProperty('token'); expect(result?.properties).not.toHaveProperty('apikey'); expect(result?.properties).not.toHaveProperty('secret'); expect(result?.properties).not.toHaveProperty('credential'); expect(result?.properties).not.toHaveProperty('auth'); expect(result?.properties).not.toHaveProperty('url'); expect(result?.properties).not.toHaveProperty('endpoint'); expect(result?.properties).not.toHaveProperty('host'); expect(result?.properties).not.toHaveProperty('database'); expect(result?.properties.normalProp).toBe('safe-value'); expect(result?.properties.count).toBe(42); expect(result?.properties.enabled).toBe(true); }); it('should handle validation_details event schema', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'validation_details', properties: { nodeType: 'nodes-base.httpRequest', errorType: 'required_field_missing', errorCategory: 'validation_error', details: { field: 'url' } } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.nodeType).toBe('nodes-base.httpRequest'); expect(result?.properties.errorType).toBe('required_field_missing'); }); it('should handle null and undefined values', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'null_event', properties: { nullValue: null, undefinedValue: undefined, normalValue: 'test' } }; const result = validator.validateEvent(event); expect(result).not.toBeNull(); expect(result?.properties.nullValue).toBeNull(); expect(result?.properties.undefinedValue).toBeNull(); expect(result?.properties.normalValue).toBe('test'); }); }); describe('validateWorkflow()', () => { it('should validate a valid workflow', () => { const workflow: WorkflowTelemetry = { user_id: 'user123', workflow_hash: 'hash123', node_count: 3, node_types: ['webhook', 'httpRequest', 'set'], has_trigger: true, has_webhook: true, complexity: 'medium', sanitized_workflow: { nodes: [ { id: '1', type: 'webhook' }, { id: '2', type: 'httpRequest' }, { id: '3', type: 'set' } ], connections: { '1': { main: [[{ node: '2', type: 'main', index: 0 }]] } } } }; const result = validator.validateWorkflow(workflow); expect(result).toEqual(workflow); }); it('should reject workflow with too many nodes', () => { const workflow: WorkflowTelemetry = { user_id: 'user123', workflow_hash: 'hash123', node_count: 1001, // Over limit node_types: ['webhook'], has_trigger: true, has_webhook: true, complexity: 'complex', sanitized_workflow: { nodes: [], connections: {} } }; const result = validator.validateWorkflow(workflow); expect(result).toBeNull(); }); it('should reject workflow with invalid complexity', () => { const workflow = { user_id: 'user123', workflow_hash: 'hash123', node_count: 3, node_types: ['webhook'], has_trigger: true, has_webhook: true, complexity: 'invalid' as any, // Invalid complexity sanitized_workflow: { nodes: [], connections: {} } }; const result = validator.validateWorkflow(workflow); expect(result).toBeNull(); }); it('should reject workflow with too many node types', () => { const workflow: WorkflowTelemetry = { user_id: 'user123', workflow_hash: 'hash123', node_count: 3, node_types: Array.from({ length: 101 }, (_, i) => `node-${i}`), // Over limit has_trigger: true, has_webhook: true, complexity: 'complex', sanitized_workflow: { nodes: [], connections: {} } }; const result = validator.validateWorkflow(workflow); expect(result).toBeNull(); }); }); describe('getStats()', () => { it('should track validation statistics', () => { const validEvent: TelemetryEvent = { user_id: 'user123', event: 'valid_event', properties: {} }; const invalidEvent: TelemetryEvent = { user_id: '', // Invalid event: 'invalid_event', properties: {} }; validator.validateEvent(validEvent); validator.validateEvent(validEvent); validator.validateEvent(invalidEvent); const stats = validator.getStats(); expect(stats.successes).toBe(2); expect(stats.errors).toBe(1); expect(stats.total).toBe(3); expect(stats.errorRate).toBeCloseTo(0.333, 3); }); it('should handle division by zero in error rate', () => { const stats = validator.getStats(); expect(stats.errorRate).toBe(0); }); }); describe('resetStats()', () => { it('should reset validation statistics', () => { const validEvent: TelemetryEvent = { user_id: 'user123', event: 'valid_event', properties: {} }; validator.validateEvent(validEvent); validator.resetStats(); const stats = validator.getStats(); expect(stats.successes).toBe(0); expect(stats.errors).toBe(0); expect(stats.total).toBe(0); expect(stats.errorRate).toBe(0); }); }); describe('Schema validation', () => { describe('telemetryEventSchema', () => { it('should validate with created_at timestamp', () => { const event = { user_id: 'user123', event: 'test_event', properties: {}, created_at: '2024-01-01T00:00:00Z' }; const result = telemetryEventSchema.safeParse(event); expect(result.success).toBe(true); }); it('should reject invalid datetime format', () => { const event = { user_id: 'user123', event: 'test_event', properties: {}, created_at: 'invalid-date' }; const result = telemetryEventSchema.safeParse(event); expect(result.success).toBe(false); }); it('should enforce user_id length limits', () => { const longUserId = 'a'.repeat(65); const event = { user_id: longUserId, event: 'test_event', properties: {} }; const result = telemetryEventSchema.safeParse(event); expect(result.success).toBe(false); }); it('should enforce event name regex pattern', () => { const event = { user_id: 'user123', event: 'invalid event name with spaces!', properties: {} }; const result = telemetryEventSchema.safeParse(event); expect(result.success).toBe(false); }); }); describe('workflowTelemetrySchema', () => { it('should enforce node array size limits', () => { const workflow = { user_id: 'user123', workflow_hash: 'hash123', node_count: 3, node_types: ['test'], has_trigger: true, has_webhook: false, complexity: 'simple', sanitized_workflow: { nodes: Array.from({ length: 1001 }, (_, i) => ({ id: i })), // Over limit connections: {} } }; const result = workflowTelemetrySchema.safeParse(workflow); expect(result.success).toBe(false); }); it('should validate with optional created_at', () => { const workflow = { user_id: 'user123', workflow_hash: 'hash123', node_count: 1, node_types: ['webhook'], has_trigger: true, has_webhook: true, complexity: 'simple', sanitized_workflow: { nodes: [{ id: '1' }], connections: {} }, created_at: '2024-01-01T00:00:00Z' }; const result = workflowTelemetrySchema.safeParse(workflow); expect(result.success).toBe(true); }); }); }); describe('String sanitization edge cases', () => { it('should handle multiple URLs in same string', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'test_event', properties: { description: 'Visit https://example.com or http://test.com for more info' } }; const result = validator.validateEvent(event); expect(result?.properties.description).toBe('Visit [URL] or [URL] for more info'); }); it('should handle mixed sensitive content', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'test_event', properties: { message: 'Contact admin@example.com at https://secure.com with key abc123def456ghi789jkl012mno345pqr' } }; const result = validator.validateEvent(event); expect(result?.properties.message).toBe('Contact [EMAIL] at [URL] with key [KEY]'); }); it('should preserve non-sensitive content', () => { const event: TelemetryEvent = { user_id: 'user123', event: 'test_event', properties: { status: 'success', count: 42, enabled: true, short_id: 'abc123' // Too short to be considered a key } }; const result = validator.validateEvent(event); expect(result?.properties.status).toBe('success'); expect(result?.properties.count).toBe(42); expect(result?.properties.enabled).toBe(true); expect(result?.properties.short_id).toBe('abc123'); }); }); describe('Error handling', () => { it('should handle Zod parsing errors gracefully', () => { const invalidEvent = { user_id: 123, // Should be string event: 'test_event', properties: {} }; const result = validator.validateEvent(invalidEvent as any); expect(result).toBeNull(); }); it('should handle unexpected errors during validation', () => { const eventWithCircularRef: any = { user_id: 'user123', event: 'test_event', properties: {} }; // Create circular reference eventWithCircularRef.properties.self = eventWithCircularRef; const result = validator.validateEvent(eventWithCircularRef); // Should handle gracefully and not throw expect(result).not.toThrow; }); }); });

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