#!/usr/bin/env node // ============================================ // Full Integration Test for LogViewer // Tests: Collector, Viewer, Monitor, Self-Forward, Telegram // ============================================ const http = require('http'); const COLLECTOR = 'http://localhost:8081'; const VIEWER = 'http://localhost:9090'; const MONITOR = 'http://localhost:8082'; let passed = 0, failed = 0; function post(base, path, data) { return new Promise((resolve, reject) => { const payload = JSON.stringify(data); const url = new URL(path, base); const req = http.request({ hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, }, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } }); }); req.on('error', reject); req.write(payload); req.end(); }); } function postText(base, path, text) { return new Promise((resolve, reject) => { const url = new URL(path, base); const req = http.request({ hostname: url.hostname, port: url.port, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'text/plain', 'Content-Length': Buffer.byteLength(text) }, }, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } }); }); req.on('error', reject); req.write(text); req.end(); }); } function get(base, path) { return new Promise((resolve, reject) => { const url = new URL(path, base); http.get({ hostname: url.hostname, port: url.port, path: url.pathname + url.search }, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } }); }).on('error', reject); }); } function del(base, path) { return new Promise((resolve, reject) => { const url = new URL(path, base); const req = http.request({ hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: 'DELETE' }, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => { try { resolve(JSON.parse(body)); } catch { resolve(body); } }); }); req.on('error', reject); req.end(); }); } function check(name, condition) { if (condition) { console.log(` ✅ ${name}`); passed++; } else { console.log(` ❌ ${name}`); failed++; } } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function testCollector() { console.log('\n ═══ COLLECTOR TESTS ═══\n'); // 1. Health check const mode = await get(COLLECTOR, '/api/mode'); check('Collector is running', mode && mode.mode === 'collector'); check('Collector is active', mode && mode.hasCollector === true); // 2. Send plain text logs const r1 = await postText(COLLECTOR, '/collect/test-plain', '2026-05-01T10:00:00Z INFO Test line 1\n2026-05-01T10:00:01Z ERROR Test line 2'); check('Plain text ingest', r1 && r1.ok === true && r1.accepted === 2); // 3. Send JSON logs const r2 = await post(COLLECTOR, '/collect/test-json/json', ['INFO json line 1', 'WARN json line 2', 'ERROR json line 3']); check('JSON ingest', r2 && r2.ok === true && r2.accepted === 3); // 4. List streams const streams = await get(COLLECTOR, '/collector/streams'); check('Streams listed', Array.isArray(streams) && streams.length >= 2); check('test-plain stream exists', streams.some(s => s.id === 'test-plain')); check('test-json stream exists', streams.some(s => s.id === 'test-json')); // 5. Read stream const data = await get(COLLECTOR, '/collector/stream/test-plain'); check('Read stream', data && data.lines && data.lines.length === 2); // 6. Read with tail const tail = await get(COLLECTOR, '/collector/stream/test-json?tail=1'); check('Tail parameter works', tail && tail.lines && tail.lines.length === 1); // 7. Self-forward: send to stream-a, should appear in stream-all // First we need to configure forward targets - this requires restart, so we test via direct ingest const r3 = await postText(COLLECTOR, '/collect/stream-a', 'INFO from stream-a'); check('Stream-a ingest', r3 && r3.ok === true); // 8. Delete stream const delResult = await del(COLLECTOR, '/collector/stream/test-plain'); check('Delete stream', delResult && delResult.ok === true); const afterDel = await get(COLLECTOR, '/collector/streams'); check('Stream removed', !afterDel.some(s => s.id === 'test-plain')); // 9. Collector config endpoint const config = await get(COLLECTOR, '/collector/config'); check('Config endpoint', config && config.storageDir && config.rotation); // 10. Bulk ingest const lines = []; for (let i = 0; i < 50; i++) lines.push(`2026-05-01T10:${String(i).padStart(2,'0')}:00Z INFO Bulk line ${i}`); const r4 = await post(COLLECTOR, '/collect/bulk-test/json', lines); check('Bulk ingest (50 lines)', r4 && r4.accepted === 50); } async function testViewer() { console.log('\n ═══ VIEWER TESTS ═══\n'); // 1. Health check const mode = await get(VIEWER, '/api/mode'); check('Viewer is running', mode && mode.mode === 'viewer'); // 2. HTML page served const html = await new Promise((resolve, reject) => { http.get(`${VIEWER}/`, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, body })); }).on('error', reject); }); check('HTML page served (200)', html.status === 200); check('HTML contains Log Viewer', html.body.includes('Log Viewer')); // 3. Sources listed const sources = await get(VIEWER, '/api/sources'); check('File sources listed', Array.isArray(sources) && sources.length > 0); // 4. Read logs const logs = await get(VIEWER, '/api/logs'); check('Logs readable', Array.isArray(logs) && logs.length > 0); check('Log entries have structure', logs[0] && logs[0].line && logs[0].source); // 5. Read specific source if (sources.length > 0) { const src = sources[0].path; const srcLogs = await get(VIEWER, `/api/logs?source=${encodeURIComponent(src)}`); check('Source-specific logs', Array.isArray(srcLogs) && srcLogs.length > 0); } // 6. Remote streams from collector const remote = await get(VIEWER, '/api/remote/streams'); check('Remote streams accessible', Array.isArray(remote)); if (remote.length > 0) { const stream = remote[0]; const remoteData = await get(VIEWER, `/api/remote/stream/${stream.id}`); check('Remote stream readable', Array.isArray(remoteData) && remoteData.length > 0); } else { check('Remote stream readable (skipped - no streams)', true); } // 7. Config endpoint const config = await get(VIEWER, '/api/config'); check('Config endpoint', config && config.theme); // 8. Add source const addResult = await post(VIEWER, '/api/sources', { path: 'sample-logs/security-audit.log' }); check('Add source', Array.isArray(addResult)); } async function testMonitor() { console.log('\n ═══ MONITOR TESTS ═══\n'); // 1. Health check const mode = await get(MONITOR, '/api/mode'); check('Monitor is running', mode && mode.mode === 'monitor'); // 2. HTML page served (monitor UI) const html = await new Promise((resolve, reject) => { http.get(`${MONITOR}/`, (res) => { let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, body })); }).on('error', reject); }); check('Monitor HTML served (200)', html.status === 200); check('Monitor HTML contains Monitor', html.body.includes('Log Monitor')); // 3. Create rule const rule = await post(MONITOR, '/api/monitor/rules', { id: 'test-rule-1', name: 'Test: Error Detection', enabled: true, sources: ['sample-logs/app.log'], patterns: ['\\bERROR\\b', '\\bFATAL\\b'], patternLogic: 'any', timeRange: { type: 'all' }, notifications: [], maxLinesInMessage: 10, cooldownMs: 5000, schedule: { mode: 'interval' }, }); check('Create rule', rule && rule.ok === true); // 4. Create second rule const rule2 = await post(MONITOR, '/api/monitor/rules', { id: 'test-rule-2', name: 'Test: Security Audit', enabled: true, sources: ['sample-logs/security-audit.log'], patterns: ['root.*login', 'DROP TABLE', 'GRANT'], patternLogic: 'any', timeRange: { type: 'all' }, notifications: [{ type: 'telegram', botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ', chatId: '8599028500' }], maxLinesInMessage: 5, cooldownMs: 5000, schedule: { mode: 'daily', value: '07:00' }, }); check('Create rule with Telegram', rule2 && rule2.ok === true); // 5. List rules const rules = await get(MONITOR, '/api/monitor/rules'); check('Rules listed', Array.isArray(rules) && rules.length >= 2); // 6. Trigger rule 1 const result1 = await post(MONITOR, '/api/monitor/trigger/test-rule-1', {}); check('Trigger rule 1 (errors in app.log)', result1 && result1.totalMatches > 0); if (result1 && result1.totalMatches) { console.log(` → Found ${result1.totalMatches} ERROR/FATAL lines`); } // 7. Trigger rule 2 (with Telegram) const result2 = await post(MONITOR, '/api/monitor/trigger/test-rule-2', {}); check('Trigger rule 2 (security + Telegram)', result2 && result2.totalMatches > 0); if (result2 && result2.totalMatches) { console.log(` → Found ${result2.totalMatches} security matches, Telegram alert sent`); } // 8. Check results const results = await get(MONITOR, '/api/monitor/results'); check('Results stored', Array.isArray(results) && results.length >= 2); // 9. Update rule const updated = await new Promise((resolve, reject) => { const payload = JSON.stringify({ name: 'Test: Error Detection (updated)' }); const req = http.request({ hostname: 'localhost', port: 8082, path: '/api/monitor/rule/test-rule-1', method: 'PUT', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }, }, (res) => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve(JSON.parse(b))); }); req.on('error', reject); req.write(payload); req.end(); }); check('Update rule', updated && updated.ok === true); // 10. Test notification endpoint const notifTest = await post(MONITOR, '/api/monitor/test-notification', { type: 'telegram', botToken: '8116790669:AAE_llOFDFSwDhPx40vZMBa6dGv3Xxa9ZiQ', chatId: '8599028500', }); check('Test notification (Telegram)', notifTest && notifTest.ok === true); // 11. Delete rule const delResult = await del(MONITOR, '/api/monitor/rule/test-rule-1'); check('Delete rule', delResult && delResult.ok === true); // 12. Monitor config const config = await get(MONITOR, '/api/monitor/config'); check('Monitor config endpoint', config && config.scanIntervalMs); } async function main() { console.log('\n ╔══════════════════════════════════════════╗'); console.log(' ║ LogViewer Full Integration Test ║'); console.log(' ╚══════════════════════════════════════════╝'); try { await testCollector(); } catch (e) { console.log(` ❌ Collector tests failed: ${e.message}`); failed++; } try { await testViewer(); } catch (e) { console.log(` ❌ Viewer tests failed: ${e.message}`); failed++; } try { await testMonitor(); } catch (e) { console.log(` ❌ Monitor tests failed: ${e.message}`); failed++; } console.log('\n ═══════════════════════════════════════════'); console.log(` Results: ${passed} passed, ${failed} failed`); console.log(' ═══════════════════════════════════════════\n'); process.exit(failed > 0 ? 1 : 0); } main();