/** * ╔══════════════════════════════════════════════════════════════════════════════╗ * ║ UNIFIED DATA SERVICE - ENTERPRISE TEST SUITE ║ * ╠══════════════════════════════════════════════════════════════════════════════╣ * ║ Comprehensive test coverage for the frontend data layer ║ * ║ Tests: Caching, WebSocket, Telemetry, Error Handling ║ * ╚══════════════════════════════════════════════════════════════════════════════╝ */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; // Mock WebSocket class MockWebSocket { static instances: MockWebSocket[] = []; readyState = 1; // OPEN onopen: (() => void) | null = null; onmessage: ((event: { data: string }) => void) | null = null; onclose: (() => void) | null = null; onerror: ((error: Error) => void) | null = null; constructor(public url: string) { MockWebSocket.instances.push(this); setTimeout(() => this.onopen?.(), 10); } send(data: string) { // Mock send } close() { this.readyState = 3; } simulateMessage(data: any) { this.onmessage?.({ data: JSON.stringify(data) }); } } (global as any).WebSocket = MockWebSocket; // ═══════════════════════════════════════════════════════════════════════════════ // CACHE TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - Caching', () => { beforeEach(() => { vi.clearAllMocks(); MockWebSocket.instances = []; }); it('should cache API responses', async () => { const mockData = { sources: [{ id: 'test', name: 'Test Source' }] }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockData, }); // First call should hit the API const response1 = await fetch('/api/autonomous/sources'); const data1 = await response1.json(); expect(mockFetch).toHaveBeenCalledTimes(1); expect(data1).toEqual(mockData); }); it('should return cached data for subsequent calls within TTL', async () => { const mockData = { result: 'cached' }; mockFetch.mockResolvedValue({ ok: true, json: async () => mockData, }); // Simulate cache behavior const cache = new Map(); const cacheKey = 'test-key'; const TTL = 30000; // First call - should cache cache.set(cacheKey, { data: mockData, timestamp: Date.now() }); // Second call - should use cache const cached = cache.get(cacheKey); const isValid = cached && (Date.now() - cached.timestamp) < TTL; expect(isValid).toBe(true); expect(cached?.data).toEqual(mockData); }); it('should invalidate cache after TTL expires', () => { const cache = new Map(); const cacheKey = 'expired-key'; const TTL = 30000; // Set expired cache entry cache.set(cacheKey, { data: { old: true }, timestamp: Date.now() - TTL - 1000 }); const cached = cache.get(cacheKey); const isValid = cached && (Date.now() - cached.timestamp) < TTL; expect(isValid).toBe(false); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // WEBSOCKET TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - WebSocket', () => { beforeEach(() => { vi.clearAllMocks(); MockWebSocket.instances = []; }); it('should connect to WebSocket endpoint', () => { const ws = new MockWebSocket('ws://localhost:3001/mcp/ws'); expect(ws.url).toBe('ws://localhost:3001/mcp/ws'); expect(MockWebSocket.instances).toHaveLength(1); }); it('should handle incoming WebSocket messages', async () => { const ws = new MockWebSocket('ws://localhost:3001/mcp/ws'); const messageHandler = vi.fn(); ws.onmessage = (event) => { messageHandler(JSON.parse(event.data)); }; // Simulate message await new Promise(resolve => setTimeout(resolve, 20)); ws.simulateMessage({ type: 'source:health', data: { healthy: true } }); expect(messageHandler).toHaveBeenCalledWith({ type: 'source:health', data: { healthy: true } }); }); it('should handle WebSocket connection states', () => { const ws = new MockWebSocket('ws://localhost:3001/mcp/ws'); // Initially OPEN expect(ws.readyState).toBe(1); // After close ws.close(); expect(ws.readyState).toBe(3); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // TELEMETRY TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - Telemetry', () => { it('should track API call metrics', () => { const telemetry = { calls: 0, totalLatency: 0, errors: 0, cacheHits: 0, cacheMisses: 0, }; // Simulate API call const startTime = Date.now(); telemetry.calls++; // Simulate latency const latency = 150; telemetry.totalLatency += latency; expect(telemetry.calls).toBe(1); expect(telemetry.totalLatency).toBe(150); }); it('should calculate cache hit rate', () => { const telemetry = { cacheHits: 75, cacheMisses: 25, }; const hitRate = telemetry.cacheHits / (telemetry.cacheHits + telemetry.cacheMisses) * 100; expect(hitRate).toBe(75); }); it('should track error count', () => { const telemetry = { errors: 0 }; // Simulate errors telemetry.errors++; telemetry.errors++; expect(telemetry.errors).toBe(2); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // RETRY LOGIC TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - Retry Logic', () => { it('should implement exponential backoff', () => { const calculateBackoff = (attempt: number, baseDelay: number = 1000) => { return Math.min(baseDelay * Math.pow(2, attempt), 30000); }; expect(calculateBackoff(0)).toBe(1000); expect(calculateBackoff(1)).toBe(2000); expect(calculateBackoff(2)).toBe(4000); expect(calculateBackoff(3)).toBe(8000); expect(calculateBackoff(5)).toBe(30000); // Capped at 30s }); it('should retry on network errors', async () => { let attempts = 0; const maxRetries = 3; const fetchWithRetry = async () => { while (attempts < maxRetries) { try { attempts++; if (attempts < 3) { throw new Error('Network error'); } return { success: true }; } catch (error: any) { if (attempts >= maxRetries) throw error; // continue to next attempt } } throw new Error('Max retries exceeded'); }; const result = await fetchWithRetry(); expect(attempts).toBe(3); expect(result).toEqual({ success: true }); }); it('should not retry on 4xx errors', () => { const shouldRetry = (status: number) => { return status >= 500 || status === 0; // Only retry server errors or network failures }; expect(shouldRetry(400)).toBe(false); expect(shouldRetry(401)).toBe(false); expect(shouldRetry(404)).toBe(false); expect(shouldRetry(500)).toBe(true); expect(shouldRetry(503)).toBe(true); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // DATA TRANSFORMATION TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - Data Transformation', () => { it('should transform source health data', () => { const rawData = { sources: [ { name: 'neo4j', status: 'healthy', latency: 50 }, { name: 'postgres', status: 'degraded', latency: 200 }, ] }; const transformed = rawData.sources.map(source => ({ ...source, statusColor: source.status === 'healthy' ? 'green' : source.status === 'degraded' ? 'yellow' : 'red', latencyCategory: source.latency < 100 ? 'fast' : source.latency < 500 ? 'slow' : 'critical' })); expect(transformed[0].statusColor).toBe('green'); expect(transformed[0].latencyCategory).toBe('fast'); expect(transformed[1].statusColor).toBe('yellow'); expect(transformed[1].latencyCategory).toBe('slow'); }); it('should aggregate decision statistics', () => { const decisions = [ { source: 'neo4j', success: true, latency: 50 }, { source: 'neo4j', success: true, latency: 75 }, { source: 'postgres', success: false, latency: 300 }, { source: 'neo4j', success: true, latency: 60 }, ]; const stats = decisions.reduce((acc, d) => { if (!acc[d.source]) { acc[d.source] = { count: 0, successes: 0, totalLatency: 0 }; } acc[d.source].count++; if (d.success) acc[d.source].successes++; acc[d.source].totalLatency += d.latency; return acc; }, {} as Record); expect(stats['neo4j'].count).toBe(3); expect(stats['neo4j'].successes).toBe(3); expect(stats['postgres'].count).toBe(1); expect(stats['postgres'].successes).toBe(0); }); }); // ═══════════════════════════════════════════════════════════════════════════════ // ERROR HANDLING TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('UnifiedDataService - Error Handling', () => { it('should handle network errors gracefully', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); try { await fetch('/api/test'); } catch (error: any) { expect(error.message).toBe('Network error'); } }); it('should handle JSON parse errors', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => { throw new SyntaxError('Unexpected token'); }, }); const response = await fetch('/api/test'); try { await response.json(); } catch (error: any) { expect(error).toBeInstanceOf(SyntaxError); } }); it('should handle 500 errors with retry', async () => { let attemptCount = 0; mockFetch.mockImplementation(async () => { attemptCount++; if (attemptCount < 3) { return { ok: false, status: 500 }; } return { ok: true, json: async () => ({ success: true }) }; }); // Simulate retry logic let result = null; for (let i = 0; i < 3; i++) { const response = await fetch('/api/test'); if (response.ok) { result = await response.json(); break; } } expect(attemptCount).toBe(3); expect(result).toEqual({ success: true }); }); });