307 lines
12 KiB
JavaScript
307 lines
12 KiB
JavaScript
#!/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();
|