test: build comprehensive test suite reaching 80%+ coverage threshold

Add 22 test files (~700 tests) covering security-critical modules, core
infrastructure, API routes, and error handling. Final coverage: 86.73%
statements / 80.57% branches / 85.57% functions / 87.42% lines, all above
the 80% threshold enforced by jest.config.js.

Highlights:
- Unit tests for crypto-utils, credential-manager, auth-manager, csrf,
  input-validator, state-manager, health-checker, backup-manager,
  update-manager, resource-monitor, app-templates, platform-paths,
  port-lock-manager, errors, error-handler, pagination, url-resolver
- Route tests for health, services, and containers (supertest + mocked deps)
- Shared test-utils helper for mock factories and Express app builder
- npm scripts for CI: test:ci, test:unit, test:routes, test:security,
  test:changed, test:debug
- jest.config.js: expand coverage targets, add 80% threshold gate
- routes/services.js: import ValidationError and NotFoundError from errors
- .gitignore: exclude coverage/, *.bak, *.log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 21:36:46 -07:00
parent bdf3f247b1
commit ea5acfa9a2
26 changed files with 8010 additions and 3 deletions

View File

@@ -0,0 +1,472 @@
// Resource Monitor Tests
// Validates container CPU/memory/disk/network tracking, alerts, and persistence
jest.mock('dockerode');
jest.mock('fs');
const fs = require('fs');
const EventEmitter = require('events');
// Setup defaults BEFORE requiring singleton
fs.existsSync.mockReturnValue(false);
fs.readFileSync.mockReturnValue('{}');
fs.writeFileSync.mockReturnValue(undefined);
const resourceMonitor = require('../resource-monitor');
function makeStat(overrides = {}) {
return {
timestamp: new Date().toISOString(),
cpu: { percent: 15.5, usage: 500000 },
memory: { usage: 536870912, limit: 2147483648, percent: 25.0, usageMB: 512, limitMB: 2048 },
network: { rxBytes: 1048576, txBytes: 524288, rxMB: 1, txMB: 0.5 },
disk: { readBytes: 0, writeBytes: 0, readMB: 0, writeMB: 0 },
pids: 42,
...overrides
};
}
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
fs.existsSync.mockReturnValue(false);
fs.readFileSync.mockReturnValue('{}');
fs.writeFileSync.mockReturnValue(undefined);
// Reset internal state
resourceMonitor.stats.clear();
resourceMonitor.alerts.clear();
resourceMonitor.lastAlerts.clear();
resourceMonitor.monitoring = false;
if (resourceMonitor.monitoringInterval) {
clearInterval(resourceMonitor.monitoringInterval);
resourceMonitor.monitoringInterval = null;
}
});
afterEach(() => {
resourceMonitor.stop();
jest.useRealTimers();
});
describe('ResourceMonitor — container resource tracking', () => {
describe('start/stop lifecycle', () => {
it('starts monitoring', () => {
resourceMonitor.start();
expect(resourceMonitor.monitoring).toBe(true);
});
it('ignores double start', () => {
resourceMonitor.start();
resourceMonitor.start();
expect(resourceMonitor.monitoring).toBe(true);
});
it('stops monitoring and saves stats', () => {
resourceMonitor.start();
resourceMonitor.stop();
expect(resourceMonitor.monitoring).toBe(false);
expect(resourceMonitor.monitoringInterval).toBeNull();
expect(fs.writeFileSync).toHaveBeenCalled();
});
it('ignores stop when not monitoring', () => {
resourceMonitor.stop();
expect(resourceMonitor.monitoring).toBe(false);
});
});
describe('recordStats', () => {
it('creates new entry for unknown container', () => {
const stat = makeStat();
resourceMonitor.recordStats('abc123', '/plex', stat);
expect(resourceMonitor.stats.has('abc123')).toBe(true);
expect(resourceMonitor.stats.get('abc123').history).toHaveLength(1);
});
it('appends to existing container history', () => {
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.recordStats('abc123', '/plex', makeStat());
expect(resourceMonitor.stats.get('abc123').history).toHaveLength(2);
});
it('updates container name if changed', () => {
resourceMonitor.recordStats('abc123', '/plex-old', makeStat());
resourceMonitor.recordStats('abc123', '/plex-new', makeStat());
expect(resourceMonitor.stats.get('abc123').name).toBe('/plex-new');
});
it('trims stats older than retention period', () => {
const oldStat = makeStat({ timestamp: new Date(Date.now() - 999 * 60 * 60 * 1000).toISOString() });
const newStat = makeStat();
resourceMonitor.recordStats('abc123', '/plex', oldStat);
resourceMonitor.recordStats('abc123', '/plex', newStat);
// Old stat exceeds 168h (7 day) retention
expect(resourceMonitor.stats.get('abc123').history).toHaveLength(1);
});
});
describe('getCurrentStats', () => {
it('returns null for unknown container', () => {
expect(resourceMonitor.getCurrentStats('unknown')).toBeNull();
});
it('returns latest stat entry', () => {
const stat1 = makeStat({ cpu: { percent: 10, usage: 100 } });
const stat2 = makeStat({ cpu: { percent: 50, usage: 500 } });
resourceMonitor.recordStats('abc123', '/plex', stat1);
resourceMonitor.recordStats('abc123', '/plex', stat2);
expect(resourceMonitor.getCurrentStats('abc123').cpu.percent).toBe(50);
});
});
describe('getHistoricalStats', () => {
it('returns empty array for unknown container', () => {
expect(resourceMonitor.getHistoricalStats('unknown')).toEqual([]);
});
it('filters by time window', () => {
const recentStat = makeStat();
const oldStat = makeStat({ timestamp: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString() });
resourceMonitor.stats.set('abc123', {
name: '/plex',
history: [oldStat, recentStat]
});
// Only last 24 hours
const result = resourceMonitor.getHistoricalStats('abc123', 24);
expect(result).toHaveLength(1);
});
});
describe('getAggregatedStats', () => {
it('returns null for unknown container', () => {
expect(resourceMonitor.getAggregatedStats('unknown')).toBeNull();
});
it('calculates min/max/avg for CPU and memory', () => {
const stats = [
makeStat({ cpu: { percent: 10, usage: 100 }, memory: { percent: 20, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
makeStat({ cpu: { percent: 30, usage: 300 }, memory: { percent: 40, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
makeStat({ cpu: { percent: 50, usage: 500 }, memory: { percent: 60, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } }),
];
resourceMonitor.stats.set('abc123', { name: '/plex', history: stats });
const agg = resourceMonitor.getAggregatedStats('abc123', 24);
expect(agg.cpu.min).toBe(10);
expect(agg.cpu.max).toBe(50);
expect(agg.cpu.avg).toBe(30);
expect(agg.cpu.current).toBe(50);
expect(agg.memory.min).toBe(20);
expect(agg.memory.max).toBe(60);
expect(agg.dataPoints).toBe(3);
});
});
describe('getAllStats', () => {
it('returns all containers with current and aggregated data', () => {
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.recordStats('def456', '/radarr', makeStat());
const all = resourceMonitor.getAllStats();
expect(Object.keys(all)).toHaveLength(2);
expect(all['abc123'].name).toBe('/plex');
expect(all['abc123'].current).toBeDefined();
expect(all['abc123'].aggregated).toBeDefined();
});
});
describe('alert configuration', () => {
it('setAlertConfig stores config and persists', () => {
resourceMonitor.setAlertConfig('abc123', {
cpuThreshold: 80,
memoryThreshold: 90,
cooldownMinutes: 30
});
const config = resourceMonitor.getAlertConfig('abc123');
expect(config.enabled).toBe(true);
expect(config.cpuThreshold).toBe(80);
expect(config.memoryThreshold).toBe(90);
expect(config.cooldownMinutes).toBe(30);
expect(fs.writeFileSync).toHaveBeenCalled();
});
it('returns null for unconfigured container', () => {
expect(resourceMonitor.getAlertConfig('unknown')).toBeNull();
});
it('removeAlertConfig clears config and cooldown', () => {
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });
resourceMonitor.lastAlerts.set('abc123', Date.now());
resourceMonitor.removeAlertConfig('abc123');
expect(resourceMonitor.getAlertConfig('abc123')).toBeNull();
expect(resourceMonitor.lastAlerts.has('abc123')).toBe(false);
});
});
describe('checkAlerts', () => {
it('emits alert when CPU exceeds threshold', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 50, cooldownMinutes: 0 });
const stat = makeStat({ cpu: { percent: 75, usage: 750 } });
resourceMonitor.checkAlerts('abc123', '/plex', stat);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
containerId: 'abc123',
alerts: expect.arrayContaining([
expect.objectContaining({ type: 'cpu', value: 75 })
])
})
);
resourceMonitor.off('alert', handler);
});
it('emits alert when memory exceeds threshold', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.setAlertConfig('abc123', { memoryThreshold: 20, cooldownMinutes: 0 });
const stat = makeStat({ memory: { percent: 80, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } });
resourceMonitor.checkAlerts('abc123', '/plex', stat);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
alerts: expect.arrayContaining([
expect.objectContaining({ type: 'memory' })
])
})
);
resourceMonitor.off('alert', handler);
});
it('emits alert when disk I/O exceeds threshold', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.setAlertConfig('abc123', { diskIOThreshold: 10, cooldownMinutes: 0 });
const stat = makeStat({ disk: { readMB: 15, writeMB: 10, readBytes: 0, writeBytes: 0 } });
resourceMonitor.checkAlerts('abc123', '/plex', stat);
expect(handler).toHaveBeenCalledWith(
expect.objectContaining({
alerts: expect.arrayContaining([
expect.objectContaining({ type: 'disk' })
])
})
);
resourceMonitor.off('alert', handler);
});
it('respects cooldown period', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 50, cooldownMinutes: 15 });
resourceMonitor.lastAlerts.set('abc123', Date.now()); // Just alerted
const stat = makeStat({ cpu: { percent: 99, usage: 990 } });
resourceMonitor.checkAlerts('abc123', '/plex', stat);
expect(handler).not.toHaveBeenCalled();
resourceMonitor.off('alert', handler);
});
it('skips when alerts not configured or disabled', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
// No config
resourceMonitor.checkAlerts('abc123', '/plex', makeStat());
expect(handler).not.toHaveBeenCalled();
// Disabled config
resourceMonitor.alerts.set('abc123', { enabled: false, cpuThreshold: 1 });
resourceMonitor.checkAlerts('abc123', '/plex', makeStat());
expect(handler).not.toHaveBeenCalled();
resourceMonitor.off('alert', handler);
});
it('does not alert when below thresholds', () => {
const handler = jest.fn();
resourceMonitor.on('alert', handler);
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 90, memoryThreshold: 90, cooldownMinutes: 0 });
const stat = makeStat({ cpu: { percent: 5, usage: 50 }, memory: { percent: 10, usage: 0, limit: 0, usageMB: 0, limitMB: 0 } });
resourceMonitor.checkAlerts('abc123', '/plex', stat);
expect(handler).not.toHaveBeenCalled();
resourceMonitor.off('alert', handler);
});
});
describe('cleanupOldStats', () => {
it('removes containers with no recent data', () => {
const oldStat = makeStat({ timestamp: new Date(Date.now() - 999 * 60 * 60 * 1000).toISOString() });
resourceMonitor.stats.set('old-container', { name: '/old', history: [oldStat] });
resourceMonitor.cleanupOldStats();
expect(resourceMonitor.stats.has('old-container')).toBe(false);
});
it('keeps containers with recent data', () => {
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.cleanupOldStats();
expect(resourceMonitor.stats.has('abc123')).toBe(true);
});
});
describe('persistence (loadStats/saveStats)', () => {
it('loadStats populates from file', () => {
const savedData = {
'abc123': { name: '/plex', history: [makeStat()] }
};
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(JSON.stringify(savedData));
resourceMonitor.loadStats();
expect(resourceMonitor.stats.size).toBe(1);
});
it('loadStats handles missing file', () => {
fs.existsSync.mockReturnValue(false);
resourceMonitor.loadStats();
expect(resourceMonitor.stats.size).toBe(0);
});
it('loadStats handles corrupt file', () => {
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockImplementation(() => { throw new Error('corrupt'); });
resourceMonitor.loadStats(); // should not throw
});
it('saveStats writes Map as JSON object', () => {
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.saveStats();
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.any(String),
expect.stringContaining('abc123')
);
});
it('saveStats handles write error', () => {
fs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.saveStats(); // should not throw
});
});
describe('alert config persistence', () => {
it('loadAlertConfig populates from file', () => {
const config = { 'abc123': { enabled: true, cpuThreshold: 80 } };
fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockReturnValue(JSON.stringify(config));
resourceMonitor.loadAlertConfig();
expect(resourceMonitor.alerts.size).toBe(1);
});
it('saveAlertConfig writes alerts as JSON', () => {
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });
expect(fs.writeFileSync).toHaveBeenCalled();
});
});
describe('exportStats / importStats', () => {
it('exports stats and alerts', () => {
resourceMonitor.recordStats('abc123', '/plex', makeStat());
resourceMonitor.setAlertConfig('abc123', { cpuThreshold: 80 });
const exported = resourceMonitor.exportStats();
expect(exported.stats['abc123']).toBeDefined();
expect(exported.alerts['abc123']).toBeDefined();
expect(exported.exportedAt).toBeDefined();
});
it('imports stats and alerts', () => {
const data = {
stats: { 'abc123': { name: '/plex', history: [makeStat()] } },
alerts: { 'abc123': { enabled: true, cpuThreshold: 90 } }
};
resourceMonitor.importStats(data);
expect(resourceMonitor.stats.size).toBe(1);
expect(resourceMonitor.alerts.size).toBe(1);
// Should persist after import
expect(fs.writeFileSync).toHaveBeenCalled();
});
});
describe('getContainerStats (Docker stats parsing)', () => {
it('parses Docker stats into structured format', async () => {
const Docker = require('dockerode');
const mockContainer = {
stats: jest.fn((opts, cb) => cb(null, {
cpu_stats: {
cpu_usage: { total_usage: 200000 },
system_cpu_usage: 1000000
},
precpu_stats: {
cpu_usage: { total_usage: 100000 },
system_cpu_usage: 500000
},
memory_stats: {
usage: 536870912, // 512MB
limit: 2147483648 // 2GB
},
networks: {
eth0: { rx_bytes: 1048576, tx_bytes: 524288 }
},
blkio_stats: {
io_service_bytes_recursive: [
{ op: 'Read', value: 1048576 },
{ op: 'Write', value: 2097152 }
]
},
pids_stats: { current: 42 }
}))
};
const result = await resourceMonitor.getContainerStats(mockContainer);
expect(result.cpu.percent).toBe(20); // (100000/500000) * 100
expect(result.memory.usageMB).toBe(512);
expect(result.memory.limitMB).toBe(2048);
expect(result.memory.percent).toBe(25);
expect(result.network.rxMB).toBe(1);
expect(result.disk.readMB).toBe(1);
expect(result.disk.writeMB).toBe(2);
expect(result.pids).toBe(42);
});
it('handles missing network stats', async () => {
const mockContainer = {
stats: jest.fn((opts, cb) => cb(null, {
cpu_stats: { cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
precpu_stats: { cpu_usage: { total_usage: 0 }, system_cpu_usage: 0 },
memory_stats: { usage: 0, limit: 0 },
blkio_stats: {},
pids_stats: {}
}))
};
const result = await resourceMonitor.getContainerStats(mockContainer);
expect(result.network.rxBytes).toBe(0);
expect(result.network.txBytes).toBe(0);
expect(result.pids).toBe(0);
});
it('rejects on Docker error', async () => {
const mockContainer = {
stats: jest.fn((opts, cb) => cb(new Error('container gone')))
};
await expect(resourceMonitor.getContainerStats(mockContainer)).rejects.toThrow('container gone');
});
});
});