Files
dashcaddy/dashcaddy-api/__tests__/ssl-monitor.test.js
Hermes 7bc2a207f3 DC-005: Fix all 138 broken test paths after src/ refactor
After the DC-005 module reorganization (41 files moved into src/ subdirs),
138 test suites failed because the refactor script's path-rewrite logic
missed three categories:

1. Files inside src/ doing 'require("./src/...")' — should be 'require("../...")'
2. Files in src/X/Y/ doing 'require("../../../src/...")' — should be 'require("../../...")'
3. Test files in __tests__/ with leftover 'require("../../../src/...")' paths

Root cause: the original refactor script ran before all files were moved,
so it computed relative paths against stale filesystem state.

Result:
- 30/30 test suites pass
- 879/879 tests pass (was: 18/30 suites, 614/687 tests)

Also fixed:
- routes/apps/restore.js: wrong responses import path
- routes/*/*.js: '../../src/utilities/X' → '../src/utilities/X' (depth 2 routes)
2026-06-13 12:16:56 -07:00

204 lines
6.8 KiB
JavaScript

/**
* Smoke tests for ssl-monitor.js
* Verifies SSLMonitor loads, exposes the expected interface, can check
* certificates via mocked TLS, manage state, and persist cache.
*/
jest.mock('tls', () => ({
connect: jest.fn(),
}));
jest.mock('../src/utilities/fs-helpers', () => ({
readJsonFile: jest.fn().mockResolvedValue(null),
writeJsonFile: jest.fn().mockResolvedValue(undefined),
}));
const tls = require('tls');
const fsHelpers = require('../src/utilities/fs-helpers');
const SSLMonitor = require('../src/monitoring/ssl-monitor');
function makeSocket({ cert = null, error = null } = {}) {
const { EventEmitter } = require('events');
const socket = new EventEmitter();
socket.destroy = jest.fn();
socket.getPeerCertificate = jest.fn(() => cert);
socket.setTimeout = jest.fn();
// Simulate 'connect' on next tick (or 'error')
process.nextTick(() => {
if (error) socket.emit('error', error);
});
return socket;
}
describe('SSLMonitor', () => {
let monitor;
const fakeStateManager = {
read: jest.fn().mockResolvedValue([]),
};
beforeEach(() => {
jest.clearAllMocks();
fsHelpers.readJsonFile.mockResolvedValue(null);
fsHelpers.writeJsonFile.mockResolvedValue(undefined);
fakeStateManager.read.mockResolvedValue([]);
monitor = new SSLMonitor({
log: { error: jest.fn(), info: jest.fn(), warn: jest.fn() },
servicesStateManager: fakeStateManager,
siteConfig: {},
buildServiceUrl: id => `https://${id}.sami`,
notification: null,
});
});
afterEach(() => {
monitor.stop();
});
test('initializes with empty maps and default config', () => {
expect(monitor.certStatus).toBeInstanceOf(Map);
expect(monitor.notifiedThresholds).toBeInstanceOf(Map);
expect(monitor.hostnameToServiceId).toBeInstanceOf(Map);
expect(monitor.intervalHandle).toBeNull();
expect(monitor.config.enabled).toBe(true);
expect(typeof monitor.config.intervalMs).toBe('number');
});
test('getConfig returns a copy of the current config', () => {
const cfg = monitor.getConfig();
expect(cfg).toEqual(monitor.config);
cfg.enabled = false;
// The internal config must not be mutated
expect(monitor.config.enabled).toBe(true);
});
test('updateConfig updates enabled and intervalMs', () => {
monitor.updateConfig({ enabled: false, intervalMs: 60000 });
expect(monitor.config.enabled).toBe(false);
expect(monitor.config.intervalMs).toBe(60000);
});
test('updateConfig rejects intervalMs below 60000', () => {
const original = monitor.config.intervalMs;
monitor.updateConfig({ intervalMs: 1000 });
expect(monitor.config.intervalMs).toBe(original);
});
test('getStatus returns an empty object when no checks have run', () => {
expect(monitor.getStatus()).toEqual({});
});
test('getServiceCertStatus returns null for unknown service', () => {
expect(monitor.getServiceCertStatus('unknown-svc')).toBeNull();
});
test('checkCert rejects when peer cert is empty', async () => {
tls.connect.mockImplementation((_opts, onConnect) => {
const sock = makeSocket({ cert: {} });
// Simulate immediate 'connect'
setImmediate(() => onConnect && onConnect());
return sock;
});
await expect(monitor.checkCert('empty.sami')).rejects.toThrow(/No certificate/);
});
test('checkCert resolves with cert details on success', async () => {
const futureDate = new Date(Date.now() + 60 * 24 * 60 * 60 * 1000); // +60d
const validTo = futureDate.toUTCString();
tls.connect.mockImplementation((_opts, onConnect) => {
const sock = makeSocket({
cert: {
subject: { CN: 'test.sami' },
issuer: { O: "Sami's CA" },
valid_from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30).toUTCString(),
valid_to: validTo,
fingerprint: 'AA:BB:CC',
},
});
setImmediate(() => onConnect && onConnect());
return sock;
});
const result = await monitor.checkCert('test.sami', 443);
expect(result.hostname).toBe('test.sami');
expect(result.port).toBe(443);
expect(result.subject).toBe('test.sami');
expect(result.daysRemaining).toBeGreaterThan(0);
expect(typeof result.isExpiring).toBe('boolean');
expect(typeof result.checkedAt).toBe('string');
});
test('checkCert rejects with TLS error event', async () => {
tls.connect.mockImplementation(() => {
const sock = makeSocket({ error: new Error('TLS boom') });
return sock;
});
await expect(monitor.checkCert('broken.sami')).rejects.toThrow(/TLS/);
});
test('checkAll returns empty status when no services configured', async () => {
const status = await monitor.checkAll();
expect(status).toEqual({});
});
test('checkAll handles HTTPS services and stores results', async () => {
fakeStateManager.read.mockResolvedValue([
{ id: 'web', name: 'Web', url: 'https://web.sami' },
]);
const futureDate = new Date(Date.now() + 60 * 24 * 60 * 60 * 1000);
tls.connect.mockImplementation((_opts, onConnect) => {
const sock = makeSocket({
cert: {
subject: { CN: 'web.sami' },
issuer: { O: "Sami's CA" },
valid_from: new Date().toUTCString(),
valid_to: futureDate.toUTCString(),
fingerprint: 'AA:BB:CC',
},
});
setImmediate(() => onConnect && onConnect());
return sock;
});
const status = await monitor.checkAll();
expect(status['web.sami']).toBeDefined();
expect(status['web.sami'].hostname).toBe('web.sami');
expect(monitor.getServiceCertStatus('web')).not.toBeNull();
});
test('start() schedules periodic checks and stop() clears them', () => {
jest.useFakeTimers();
const originalCheckAll = monitor.checkAll.bind(monitor);
monitor.checkAll = jest.fn().mockResolvedValue(undefined);
monitor.start(120000);
expect(monitor.intervalHandle).not.toBeNull();
monitor.stop();
expect(monitor.intervalHandle).toBeNull();
monitor.checkAll = originalCheckAll;
jest.useRealTimers();
});
test('_saveCache and _loadCache round-trip via fs-helpers', async () => {
await monitor._saveCache();
expect(fsHelpers.writeJsonFile).toHaveBeenCalled();
fsHelpers.readJsonFile.mockResolvedValue({
lastChecked: new Date().toISOString(),
certs: { 'a.sami': { hostname: 'a.sami', daysRemaining: 30 } },
hostnameToServiceId: { 'a.sami': 'svc-a' },
});
const fresh = new SSLMonitor({
log: { error: jest.fn(), info: jest.fn(), warn: jest.fn() },
servicesStateManager: fakeStateManager,
siteConfig: {},
buildServiceUrl: id => `https://${id}.sami`,
});
await fresh._loadCache();
expect(fresh.certStatus.get('a.sami')).toBeDefined();
expect(fresh.hostnameToServiceId.get('a.sami')).toBe('svc-a');
});
});