DC-007: Add smoke tests for 7 untested modules
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 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('../fs-helpers', () => ({
|
||||
readJsonFile: jest.fn().mockResolvedValue(null),
|
||||
writeJsonFile: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const tls = require('tls');
|
||||
const fsHelpers = require('../fs-helpers');
|
||||
const SSLMonitor = require('../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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user