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)
208 lines
8.0 KiB
JavaScript
208 lines
8.0 KiB
JavaScript
/**
|
|
* Smoke tests for metrics.js
|
|
* Verifies the Metrics singleton exposes the expected interface, accumulates
|
|
* request/error/business counters, normalizes paths, formats uptime, and resets.
|
|
*
|
|
* The module exports a singleton instance, so we import it once and mutate its
|
|
* state in beforeEach.
|
|
*/
|
|
|
|
const metrics = require('../src/monitoring/metrics');
|
|
|
|
describe('Metrics (singleton)', () => {
|
|
beforeEach(() => {
|
|
metrics.reset();
|
|
});
|
|
|
|
test('exposes the documented public API', () => {
|
|
expect(typeof metrics.recordRequest).toBe('function');
|
|
expect(typeof metrics.recordError).toBe('function');
|
|
expect(typeof metrics.recordBusinessEvent).toBe('function');
|
|
expect(typeof metrics.normalizePath).toBe('function');
|
|
expect(typeof metrics.getSummary).toBe('function');
|
|
expect(typeof metrics.formatUptime).toBe('function');
|
|
expect(typeof metrics.reset).toBe('function');
|
|
});
|
|
|
|
describe('recordRequest', () => {
|
|
test('increments total request count', () => {
|
|
metrics.recordRequest('GET', '/api/services', 200, 12);
|
|
metrics.recordRequest('GET', '/api/services', 200, 8);
|
|
expect(metrics.requests.total).toBe(2);
|
|
});
|
|
|
|
test('aggregates by status code', () => {
|
|
metrics.recordRequest('GET', '/a', 200, 5);
|
|
metrics.recordRequest('GET', '/b', 200, 5);
|
|
metrics.recordRequest('POST', '/c', 500, 5);
|
|
expect(metrics.requests.byStatus[200]).toBe(2);
|
|
expect(metrics.requests.byStatus[500]).toBe(1);
|
|
});
|
|
|
|
test('aggregates by HTTP method', () => {
|
|
metrics.recordRequest('GET', '/a', 200, 1);
|
|
metrics.recordRequest('GET', '/b', 200, 1);
|
|
metrics.recordRequest('DELETE', '/c', 200, 1);
|
|
expect(metrics.requests.byMethod.GET).toBe(2);
|
|
expect(metrics.requests.byMethod.DELETE).toBe(1);
|
|
});
|
|
|
|
test('aggregates by normalized path with totalDuration', () => {
|
|
// Real-looking UUID and long hex hash; both should normalize to /:id
|
|
const id1 = '550e8400-e29b-41d4-a716-446655440000';
|
|
const id2 = 'abcdef0123456789abcdef0123456789';
|
|
metrics.recordRequest('GET', `/api/services/${id1}`, 200, 10);
|
|
metrics.recordRequest('GET', `/api/services/${id2}`, 200, 20);
|
|
const entry = metrics.requests.byPath['/api/services/:id'];
|
|
expect(entry).toBeDefined();
|
|
expect(entry.count).toBe(2);
|
|
expect(entry.totalDuration).toBe(30);
|
|
});
|
|
});
|
|
|
|
describe('recordError', () => {
|
|
test('increments total error count and per-type counts', () => {
|
|
metrics.recordError('ValidationError');
|
|
metrics.recordError('ValidationError');
|
|
metrics.recordError('DockerError');
|
|
expect(metrics.errors.total).toBe(3);
|
|
expect(metrics.errors.byType.ValidationError).toBe(2);
|
|
expect(metrics.errors.byType.DockerError).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('recordBusinessEvent', () => {
|
|
test('increments known business counters', () => {
|
|
metrics.recordBusinessEvent('containersDeployed');
|
|
metrics.recordBusinessEvent('containersDeployed');
|
|
metrics.recordBusinessEvent('dnsRecordsCreated');
|
|
expect(metrics.business.containersDeployed).toBe(2);
|
|
expect(metrics.business.dnsRecordsCreated).toBe(1);
|
|
});
|
|
|
|
test('ignores unknown event types without throwing', () => {
|
|
expect(() => metrics.recordBusinessEvent('not-a-real-event')).not.toThrow();
|
|
expect(metrics.business.notARealEvent).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('normalizePath', () => {
|
|
test('replaces UUIDs with /:id', () => {
|
|
const normalized = metrics.normalizePath('/api/services/550e8400-e29b-41d4-a716-446655440000');
|
|
expect(normalized).toBe('/api/services/:id');
|
|
});
|
|
|
|
test('replaces long hex segments with /:id', () => {
|
|
expect(metrics.normalizePath('/api/containers/abc123def4567890'))
|
|
.toBe('/api/containers/:id');
|
|
});
|
|
|
|
test('replaces numeric path segments with /:n', () => {
|
|
expect(metrics.normalizePath('/api/services/42/edit'))
|
|
.toBe('/api/services/:n/edit');
|
|
});
|
|
|
|
test('leaves static paths unchanged', () => {
|
|
expect(metrics.normalizePath('/api/health')).toBe('/api/health');
|
|
expect(metrics.normalizePath('/')).toBe('/');
|
|
});
|
|
});
|
|
|
|
describe('getSummary', () => {
|
|
test('returns an object with the documented top-level shape', () => {
|
|
const summary = metrics.getSummary();
|
|
expect(summary).toHaveProperty('uptime');
|
|
expect(summary.uptime).toHaveProperty('ms');
|
|
expect(summary.uptime).toHaveProperty('human');
|
|
expect(summary).toHaveProperty('requests');
|
|
expect(summary.requests).toHaveProperty('total');
|
|
expect(summary.requests).toHaveProperty('perSecond');
|
|
expect(summary.requests).toHaveProperty('byStatus');
|
|
expect(summary.requests).toHaveProperty('byMethod');
|
|
expect(summary.requests).toHaveProperty('topEndpoints');
|
|
expect(Array.isArray(summary.requests.topEndpoints)).toBe(true);
|
|
expect(summary).toHaveProperty('errors');
|
|
expect(summary.errors).toHaveProperty('total');
|
|
expect(summary.errors).toHaveProperty('rate');
|
|
expect(summary.errors).toHaveProperty('byType');
|
|
expect(summary).toHaveProperty('business');
|
|
expect(summary).toHaveProperty('process');
|
|
expect(summary.process).toHaveProperty('pid');
|
|
});
|
|
|
|
test('reflects recorded activity', () => {
|
|
metrics.recordRequest('GET', '/api/foo', 200, 10);
|
|
metrics.recordError('BoomError');
|
|
const summary = metrics.getSummary();
|
|
expect(summary.requests.total).toBe(1);
|
|
expect(summary.requests.byStatus[200]).toBe(1);
|
|
expect(summary.errors.total).toBe(1);
|
|
expect(summary.errors.byType.BoomError).toBe(1);
|
|
// 1 error / 1 request = 100% error rate
|
|
expect(summary.errors.rate).toBe(100);
|
|
});
|
|
|
|
test('topEndpoints is sorted by count descending and capped at 15', () => {
|
|
// /a gets 3 hits, /b gets 1, /c gets 2
|
|
metrics.recordRequest('GET', '/a', 200, 1);
|
|
metrics.recordRequest('GET', '/a', 200, 2);
|
|
metrics.recordRequest('GET', '/a', 200, 3);
|
|
metrics.recordRequest('GET', '/b', 200, 1);
|
|
metrics.recordRequest('GET', '/c', 200, 1);
|
|
metrics.recordRequest('GET', '/c', 200, 2);
|
|
const top = metrics.getSummary().requests.topEndpoints;
|
|
expect(top[0].path).toBe('/a');
|
|
expect(top[0].count).toBe(3);
|
|
expect(top[0].avgMs).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('formatUptime', () => {
|
|
test('formats seconds-only when under a minute', () => {
|
|
expect(metrics.formatUptime(0)).toBe('0s');
|
|
expect(metrics.formatUptime(45)).toBe('45s');
|
|
});
|
|
|
|
test('formats minutes and seconds when under an hour', () => {
|
|
expect(metrics.formatUptime(60)).toBe('1m 0s');
|
|
expect(metrics.formatUptime(125)).toBe('2m 5s');
|
|
});
|
|
|
|
test('formats hours/minutes/seconds when under a day', () => {
|
|
expect(metrics.formatUptime(3600)).toBe('1h 0m 0s');
|
|
expect(metrics.formatUptime(3725)).toBe('1h 2m 5s');
|
|
});
|
|
|
|
test('formats days/hours/minutes when over a day', () => {
|
|
expect(metrics.formatUptime(86400)).toBe('1d 0h 0m');
|
|
// 1 day, 2 hours, 5 minutes, 0 seconds
|
|
expect(metrics.formatUptime(86400 + 2 * 3600 + 5 * 60)).toBe('1d 2h 5m');
|
|
});
|
|
});
|
|
|
|
describe('reset', () => {
|
|
test('clears request counters and error counters', () => {
|
|
metrics.recordRequest('GET', '/x', 200, 1);
|
|
metrics.recordError('E');
|
|
metrics.reset();
|
|
expect(metrics.requests.total).toBe(0);
|
|
expect(metrics.errors.total).toBe(0);
|
|
expect(metrics.requests.byStatus).toEqual({});
|
|
expect(metrics.requests.byMethod).toEqual({});
|
|
expect(metrics.requests.byPath).toEqual({});
|
|
expect(metrics.errors.byType).toEqual({});
|
|
});
|
|
|
|
test('resets startTime so uptime is small after reset', () => {
|
|
const before = metrics.startTime;
|
|
// Sleep a tick so Date.now() moves forward
|
|
const start = Date.now();
|
|
while (Date.now() - start < 5) {} // ~5ms busy-wait
|
|
metrics.reset();
|
|
expect(metrics.startTime).toBeGreaterThanOrEqual(before);
|
|
const summary = metrics.getSummary();
|
|
expect(summary.uptime.ms).toBeLessThan(5000);
|
|
});
|
|
});
|
|
});
|