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)
188 lines
6.2 KiB
JavaScript
188 lines
6.2 KiB
JavaScript
/**
|
|
* Smoke tests for log-digest.js
|
|
* Verifies the singleton LogDigest exposes the expected interface, parses
|
|
* Docker multiplexed log streams, formats digests, and supports on-demand
|
|
* daily digest generation with mocked Docker.
|
|
*/
|
|
|
|
const fsReal = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
jest.mock('dockerode', () => {
|
|
const listContainers = jest.fn().mockResolvedValue([]);
|
|
const getContainer = jest.fn(() => ({
|
|
logs: jest.fn().mockResolvedValue(Buffer.from([])),
|
|
}));
|
|
function Docker() {}
|
|
Docker.prototype.listContainers = listContainers;
|
|
Docker.prototype.getContainer = getContainer;
|
|
return Docker;
|
|
});
|
|
|
|
jest.mock('fs', () => {
|
|
const actual = jest.requireActual('fs');
|
|
return {
|
|
...actual,
|
|
existsSync: jest.fn().mockReturnValue(true),
|
|
mkdirSync: jest.fn(),
|
|
};
|
|
});
|
|
|
|
jest.mock('../src/docker/docker-maintenance', () => ({
|
|
getDiskUsage: jest.fn().mockResolvedValue(null),
|
|
}));
|
|
|
|
const Docker = require('dockerode');
|
|
const fs = require('fs');
|
|
const logDigest = require('../src/security/log-digest');
|
|
|
|
describe('LogDigest (singleton)', () => {
|
|
let dockerInstance;
|
|
let tempDir;
|
|
|
|
beforeEach(() => {
|
|
// Each test gets a fresh Docker() mock instance
|
|
jest.clearAllMocks();
|
|
fs.existsSync.mockReturnValue(true);
|
|
// Use a real, writable temp directory so writeFile inside generateDailyDigest
|
|
// does not blow up. Each test gets a fresh dir to avoid cross-test pollution.
|
|
tempDir = fsReal.mkdtempSync(path.join(os.tmpdir(), 'dc-digest-test-'));
|
|
logDigest.hourlySummaries = [];
|
|
logDigest.lastCollect = null;
|
|
logDigest.running = false;
|
|
logDigest.digestDir = null;
|
|
if (logDigest.collectInterval) {
|
|
clearInterval(logDigest.collectInterval);
|
|
logDigest.collectInterval = null;
|
|
}
|
|
if (logDigest.digestTimeout) {
|
|
clearTimeout(logDigest.digestTimeout);
|
|
logDigest.digestTimeout = null;
|
|
}
|
|
dockerInstance = new Docker();
|
|
});
|
|
|
|
afterEach(() => {
|
|
logDigest.stop();
|
|
if (tempDir && fsReal.existsSync(tempDir)) {
|
|
fsReal.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('is an EventEmitter and exposes the documented API', () => {
|
|
expect(typeof logDigest.on).toBe('function');
|
|
expect(typeof logDigest.emit).toBe('function');
|
|
expect(typeof logDigest.start).toBe('function');
|
|
expect(typeof logDigest.stop).toBe('function');
|
|
expect(typeof logDigest.generateDailyDigest).toBe('function');
|
|
expect(typeof logDigest.getLatestDigest).toBe('function');
|
|
expect(typeof logDigest.getDigestByDate).toBe('function');
|
|
expect(typeof logDigest.getDigestText).toBe('function');
|
|
expect(typeof logDigest.listDigests).toBe('function');
|
|
expect(typeof logDigest.getLiveData).toBe('function');
|
|
expect(typeof logDigest.getStatus).toBe('function');
|
|
});
|
|
|
|
test('getStatus returns current state', () => {
|
|
const status = logDigest.getStatus();
|
|
expect(status).toEqual({
|
|
running: false,
|
|
lastCollect: null,
|
|
hourlySummaries: 0,
|
|
digestDir: null,
|
|
});
|
|
});
|
|
|
|
test('start sets running and digestDir', () => {
|
|
logDigest.start(tempDir);
|
|
expect(logDigest.running).toBe(true);
|
|
expect(logDigest.digestDir).toBe(tempDir);
|
|
});
|
|
|
|
test('start is idempotent — second call does nothing new', () => {
|
|
logDigest.start(tempDir);
|
|
const firstInterval = logDigest.collectInterval;
|
|
logDigest.start(tempDir);
|
|
expect(logDigest.collectInterval).toBe(firstInterval);
|
|
});
|
|
|
|
test('_parseDockerLogs decodes multiplexed log frames into lines', () => {
|
|
// Stream type byte: 0=stdin, 1=stdout, 2=stderr
|
|
// Header: [type, 0, 0, 0, size-BE-uint32]
|
|
function frame(streamType, text) {
|
|
const buf = Buffer.from(text, 'utf8');
|
|
const header = Buffer.alloc(8);
|
|
header[0] = streamType;
|
|
header.writeUInt32BE(buf.length, 4);
|
|
return Buffer.concat([header, buf]);
|
|
}
|
|
|
|
const multiplexed = Buffer.concat([
|
|
frame(1, 'hello world\n'),
|
|
frame(2, '2026-03-13T12:00:00.000Z an error happened\n'),
|
|
]);
|
|
|
|
const lines = logDigest._parseDockerLogs(multiplexed);
|
|
expect(lines).toHaveLength(2);
|
|
expect(lines[0]).toEqual({
|
|
stream: 'stdout',
|
|
text: 'hello world',
|
|
timestamp: null,
|
|
});
|
|
expect(lines[1].stream).toBe('stderr');
|
|
expect(lines[1].text).toBe('an error happened');
|
|
expect(lines[1].timestamp).toBe('2026-03-13T12:00:00');
|
|
});
|
|
|
|
test('generateDailyDigest with empty summaries produces minimal digest', async () => {
|
|
logDigest.start(tempDir);
|
|
const digest = await logDigest.generateDailyDigest('2099-01-01');
|
|
expect(digest.date).toBe('2099-01-01');
|
|
expect(digest.services).toEqual({});
|
|
expect(digest.summary.totalServices).toBe(0);
|
|
expect(digest.summary.totalErrors).toBe(0);
|
|
expect(Array.isArray(digest.notableEvents)).toBe(true);
|
|
|
|
// Confirm the file was actually written
|
|
const writtenPath = path.join(tempDir, 'digest-2099-01-01.log');
|
|
expect(fsReal.existsSync(writtenPath)).toBe(true);
|
|
const jsonPath = path.join(tempDir, 'digest-2099-01-01.json');
|
|
expect(fsReal.existsSync(jsonPath)).toBe(true);
|
|
});
|
|
|
|
test('getLiveData returns shape with date, hoursCollected, services', () => {
|
|
const data = logDigest.getLiveData();
|
|
expect(data).toHaveProperty('date');
|
|
expect(data).toHaveProperty('hoursCollected');
|
|
expect(data).toHaveProperty('services');
|
|
expect(data).toHaveProperty('lastCollect');
|
|
});
|
|
|
|
test('getLatestDigest returns null when digestDir is null', async () => {
|
|
logDigest.digestDir = null;
|
|
const result = await logDigest.getLatestDigest();
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('getDigestByDate returns null when no file exists', async () => {
|
|
logDigest.digestDir = '/nonexistent/path';
|
|
const result = await logDigest.getDigestByDate('2020-01-01');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('listDigests returns empty array when digestDir is null', async () => {
|
|
logDigest.digestDir = null;
|
|
const result = await logDigest.listDigests();
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
test('stop clears intervals and timeouts', () => {
|
|
logDigest.start(tempDir);
|
|
logDigest.stop();
|
|
expect(logDigest.running).toBe(false);
|
|
expect(logDigest.collectInterval).toBeNull();
|
|
expect(logDigest.digestTimeout).toBeNull();
|
|
});
|
|
});
|