Initial commit: LogMaster with Viewer Collector Monitor modes
This commit is contained in:
306
test-all.js
Normal file
306
test-all.js
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user