Files
dashcaddy/dashcaddy-api/__tests__/resource-monitor.test.js

295 lines
11 KiB
JavaScript

// resource-monitor.js creates a Docker instance at module level.
// On test machines without Docker, the constructor reads from non-existent files (returns defaults).
const resourceMonitor = require('../resource-monitor');
beforeEach(() => {
// Reset singleton state
resourceMonitor.stats = new Map();
resourceMonitor.alerts = new Map();
resourceMonitor.lastAlerts = new Map();
resourceMonitor.monitoring = false;
if (resourceMonitor.monitoringInterval) {
clearInterval(resourceMonitor.monitoringInterval);
resourceMonitor.monitoringInterval = null;
}
});
afterAll(() => {
resourceMonitor.stop();
});
// Helper: create a stat entry
function makeStat(cpu = 10, memory = 50, timestamp = new Date().toISOString()) {
return {
timestamp,
cpu: { percent: cpu, usage: cpu * 1000 },
memory: { usage: memory * 1024 * 1024, limit: 1024 * 1024 * 1024, percent: memory, usageMB: memory, limitMB: 1024 },
network: { rxBytes: 0, txBytes: 0, rxMB: 0, txMB: 0 },
disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 },
pids: 5,
};
}
describe('recordStats', () => {
test('creates new entry for unknown container', () => {
resourceMonitor.recordStats('c1', '/my-app', makeStat());
expect(resourceMonitor.stats.has('c1')).toBe(true);
expect(resourceMonitor.stats.get('c1').history).toHaveLength(1);
});
test('appends to existing history', () => {
resourceMonitor.recordStats('c1', '/my-app', makeStat());
resourceMonitor.recordStats('c1', '/my-app', makeStat());
expect(resourceMonitor.stats.get('c1').history).toHaveLength(2);
});
test('updates container name', () => {
resourceMonitor.recordStats('c1', '/old-name', makeStat());
resourceMonitor.recordStats('c1', '/new-name', makeStat());
expect(resourceMonitor.stats.get('c1').name).toBe('/new-name');
});
});
describe('getCurrentStats', () => {
test('returns null for unknown container', () => {
expect(resourceMonitor.getCurrentStats('nonexistent')).toBeNull();
});
test('returns latest history entry', () => {
const stat1 = makeStat(10);
const stat2 = makeStat(50);
resourceMonitor.recordStats('c1', '/app', stat1);
resourceMonitor.recordStats('c1', '/app', stat2);
expect(resourceMonitor.getCurrentStats('c1').cpu.percent).toBe(50);
});
});
describe('getHistoricalStats', () => {
test('returns empty array for unknown container', () => {
expect(resourceMonitor.getHistoricalStats('nonexistent')).toEqual([]);
});
test('filters by time window', () => {
const recent = makeStat(10, 50, new Date().toISOString());
const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString());
resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] });
const result = resourceMonitor.getHistoricalStats('c1', 24);
expect(result).toHaveLength(1);
expect(result[0]).toBe(recent);
});
});
describe('getAggregatedStats', () => {
test('returns null for unknown container', () => {
expect(resourceMonitor.getAggregatedStats('nonexistent')).toBeNull();
});
test('returns null when no recent history', () => {
const old = makeStat(10, 50, new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString());
resourceMonitor.stats.set('c1', { name: '/app', history: [old] });
expect(resourceMonitor.getAggregatedStats('c1', 24)).toBeNull();
});
test('calculates correct avg/min/max for CPU', () => {
const now = new Date().toISOString();
resourceMonitor.stats.set('c1', {
name: '/app',
history: [makeStat(10, 50, now), makeStat(30, 50, now), makeStat(20, 50, now)],
});
const agg = resourceMonitor.getAggregatedStats('c1', 24);
expect(agg.cpu.avg).toBe(20);
expect(agg.cpu.min).toBe(10);
expect(agg.cpu.max).toBe(30);
});
test('calculates correct avg/min/max for memory', () => {
const now = new Date().toISOString();
resourceMonitor.stats.set('c1', {
name: '/app',
history: [makeStat(10, 40, now), makeStat(10, 60, now), makeStat(10, 80, now)],
});
const agg = resourceMonitor.getAggregatedStats('c1', 24);
expect(agg.memory.avg).toBe(60);
expect(agg.memory.min).toBe(40);
expect(agg.memory.max).toBe(80);
});
test('includes dataPoints and timeRange', () => {
const now = new Date().toISOString();
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
const agg = resourceMonitor.getAggregatedStats('c1', 24);
expect(agg.dataPoints).toBe(1);
expect(agg.timeRange).toBe(24);
});
});
describe('checkAlerts', () => {
test('does nothing when alert config is missing', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
expect(handler).not.toHaveBeenCalled();
resourceMonitor.removeListener('alert', handler);
});
test('does nothing when alerts are disabled', () => {
resourceMonitor.alerts.set('c1', { enabled: false, cpuThreshold: 50 });
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
expect(handler).not.toHaveBeenCalled();
resourceMonitor.removeListener('alert', handler);
});
test('triggers CPU alert when threshold exceeded', () => {
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 0 });
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(75));
expect(handler).toHaveBeenCalled();
const alertData = handler.mock.calls[0][0];
expect(alertData.alerts[0].type).toBe('cpu');
resourceMonitor.removeListener('alert', handler);
});
test('triggers memory alert when threshold exceeded', () => {
resourceMonitor.alerts.set('c1', { enabled: true, memoryThreshold: 70, cooldownMinutes: 0 });
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(10, 80));
expect(handler).toHaveBeenCalled();
const alertData = handler.mock.calls[0][0];
expect(alertData.alerts[0].type).toBe('memory');
resourceMonitor.removeListener('alert', handler);
});
test('respects cooldown period', () => {
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 50, cooldownMinutes: 15 });
resourceMonitor.lastAlerts.set('c1', Date.now()); // Just alerted
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(99));
expect(handler).not.toHaveBeenCalled();
resourceMonitor.removeListener('alert', handler);
});
test('does not trigger when below threshold', () => {
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 90, cooldownMinutes: 0 });
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.checkAlerts('c1', '/app', makeStat(50));
expect(handler).not.toHaveBeenCalled();
resourceMonitor.removeListener('alert', handler);
});
});
describe('setAlertConfig / getAlertConfig / removeAlertConfig', () => {
test('stores alert config', () => {
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
expect(resourceMonitor.alerts.has('c1')).toBe(true);
});
test('retrieves stored config', () => {
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
const config = resourceMonitor.getAlertConfig('c1');
expect(config.cpuThreshold).toBe(80);
});
test('returns null for non-existent config', () => {
expect(resourceMonitor.getAlertConfig('nonexistent')).toBeNull();
});
test('removes config and last alert', () => {
resourceMonitor.setAlertConfig('c1', { cpuThreshold: 80 });
resourceMonitor.lastAlerts.set('c1', Date.now());
resourceMonitor.removeAlertConfig('c1');
expect(resourceMonitor.alerts.has('c1')).toBe(false);
expect(resourceMonitor.lastAlerts.has('c1')).toBe(false);
});
});
describe('getAllStats', () => {
test('returns empty object when no stats', () => {
expect(resourceMonitor.getAllStats()).toEqual({});
});
test('includes current and aggregated for each container', () => {
const now = new Date().toISOString();
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
const all = resourceMonitor.getAllStats();
expect(all['c1']).toBeDefined();
expect(all['c1'].name).toBe('/app');
expect(all['c1'].current).toBeDefined();
expect(all['c1'].aggregated).toBeDefined();
});
});
describe('exportStats / importStats', () => {
test('export returns object with stats and alerts', () => {
const now = new Date().toISOString();
resourceMonitor.stats.set('c1', { name: '/app', history: [makeStat(10, 50, now)] });
resourceMonitor.alerts.set('c1', { enabled: true, cpuThreshold: 80 });
const exported = resourceMonitor.exportStats();
expect(exported.stats).toBeDefined();
expect(exported.alerts).toBeDefined();
expect(exported.exportedAt).toBeDefined();
});
test('import restores stats from backup', () => {
const backup = {
stats: { 'c1': { name: '/app', history: [makeStat()] } },
alerts: { 'c1': { enabled: true, cpuThreshold: 80 } },
};
resourceMonitor.importStats(backup);
expect(resourceMonitor.stats.has('c1')).toBe(true);
expect(resourceMonitor.alerts.has('c1')).toBe(true);
});
});
describe('cleanupOldStats', () => {
test('removes entries older than retention period', () => {
const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString());
const recent = makeStat(10, 50, new Date().toISOString());
resourceMonitor.stats.set('c1', { name: '/app', history: [old, recent] });
resourceMonitor.cleanupOldStats();
expect(resourceMonitor.stats.get('c1').history).toHaveLength(1);
});
test('deletes container entirely when no recent data', () => {
const old = makeStat(10, 50, new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString());
resourceMonitor.stats.set('c1', { name: '/app', history: [old] });
resourceMonitor.cleanupOldStats();
expect(resourceMonitor.stats.has('c1')).toBe(false);
});
});
describe('start / stop', () => {
test('start sets monitoring flag', () => {
jest.useFakeTimers();
resourceMonitor.start();
expect(resourceMonitor.monitoring).toBe(true);
resourceMonitor.stop();
jest.useRealTimers();
});
test('stop clears interval', () => {
jest.useFakeTimers();
resourceMonitor.start();
resourceMonitor.stop();
expect(resourceMonitor.monitoring).toBe(false);
expect(resourceMonitor.monitoringInterval).toBeNull();
jest.useRealTimers();
});
test('start is idempotent', () => {
jest.useFakeTimers();
resourceMonitor.start();
const first = resourceMonitor.monitoringInterval;
resourceMonitor.start();
expect(resourceMonitor.monitoringInterval).toBe(first);
resourceMonitor.stop();
jest.useRealTimers();
});
});