Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
294
dashcaddy-api/__tests__/resource-monitor.test.js
Normal file
294
dashcaddy-api/__tests__/resource-monitor.test.js
Normal file
@@ -0,0 +1,294 @@
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user