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)
204 lines
6.8 KiB
JavaScript
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');
|
|
});
|
|
});
|