mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
|
||||
// ── Env vars must be set BEFORE the service loads (enabled flags read process.env at load time)
|
||||
process.env.DB_HOST = 'test-db-host';
|
||||
process.env.DB_PORT = '5432';
|
||||
process.env.S3_ENDPOINT = 'http://test-s3:3900';
|
||||
process.env.S3_PUBLIC_URL = 'https://cdn.example.com';
|
||||
process.env.S3_BUCKET = 'test-bucket';
|
||||
process.env.SMTP_HOST = 'smtp.test.com';
|
||||
process.env.SMTP_PORT = '587';
|
||||
process.env.REDIS_URL = 'redis://127.0.0.1:6379';
|
||||
|
||||
// ── Mock all infrastructure before the service module loads ───────────────────
|
||||
jest.mock('../../config/db.config', () => ({ authenticate: jest.fn() }));
|
||||
jest.mock('../../config/redis.config', () => ({ ping: jest.fn() }));
|
||||
jest.mock('../../services/s3.service', () => ({ ping: jest.fn() }));
|
||||
jest.mock('../../services/email.service', () => ({ ping: jest.fn() }));
|
||||
|
||||
const db = require('../../config/db.config');
|
||||
const cache = require('../../config/redis.config');
|
||||
const { ping: s3 } = require('../../services/s3.service');
|
||||
const { ping: smtp } = require('../../services/email.service');
|
||||
const { runDashboard, runReadiness } = require('../../services/health.service');
|
||||
|
||||
// ── Reset mocks between tests ─────────────────────────────────────────────────
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
// ── Preset helpers ────────────────────────────────────────────────────────────
|
||||
const resolve = (fn) => fn.mockResolvedValue();
|
||||
|
||||
function allHealthy() {
|
||||
resolve(db.authenticate);
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
resolve(smtp);
|
||||
}
|
||||
|
||||
// ── runDashboard ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('runDashboard()', () => {
|
||||
|
||||
test('all healthy → status=healthy, HTTP 200', async () => {
|
||||
allHealthy();
|
||||
const { httpStatus, body } = await runDashboard();
|
||||
expect(httpStatus).toBe(200);
|
||||
expect(body.status).toBe('healthy');
|
||||
});
|
||||
|
||||
test('database down (critical) → status=unhealthy, HTTP 503', async () => {
|
||||
db.authenticate.mockRejectedValue(new Error('Connection refused'));
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
resolve(smtp);
|
||||
|
||||
const { httpStatus, body } = await runDashboard();
|
||||
expect(httpStatus).toBe(503);
|
||||
expect(body.status).toBe('unhealthy');
|
||||
});
|
||||
|
||||
test('S3 down (non-critical) → status=degraded, HTTP 200', async () => {
|
||||
resolve(db.authenticate);
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
s3.mockRejectedValue(new Error('S3 storage unavailable'));
|
||||
resolve(smtp);
|
||||
|
||||
const { httpStatus, body } = await runDashboard();
|
||||
expect(httpStatus).toBe(200);
|
||||
expect(body.status).toBe('degraded');
|
||||
});
|
||||
|
||||
test('SMTP down (non-critical) → degraded, HTTP 200', async () => {
|
||||
resolve(db.authenticate);
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
smtp.mockRejectedValue(new Error('SMTP unreachable'));
|
||||
|
||||
const { httpStatus, body } = await runDashboard();
|
||||
expect(httpStatus).toBe(200);
|
||||
expect(body.status).toBe('degraded');
|
||||
});
|
||||
|
||||
test('both DB and S3 down → unhealthy (critical takes precedence), HTTP 503', async () => {
|
||||
db.authenticate.mockRejectedValue(new Error('refused'));
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
s3.mockRejectedValue(new Error('s3 down'));
|
||||
resolve(smtp);
|
||||
|
||||
const { httpStatus, body } = await runDashboard();
|
||||
expect(httpStatus).toBe(503);
|
||||
expect(body.status).toBe('unhealthy');
|
||||
});
|
||||
|
||||
test('body has app, system, uptime, timestamp, services', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runDashboard();
|
||||
expect(body).toHaveProperty('app');
|
||||
expect(body).toHaveProperty('system');
|
||||
expect(body).toHaveProperty('uptime');
|
||||
expect(body).toHaveProperty('timestamp');
|
||||
expect(body).toHaveProperty('services');
|
||||
});
|
||||
|
||||
test('app info has correct shape', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runDashboard();
|
||||
expect(body.app).toMatchObject({
|
||||
name: expect.any(String),
|
||||
version: expect.any(String),
|
||||
environment: expect.any(String),
|
||||
node_version: expect.stringMatching(/^v\d+/),
|
||||
pid: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
test('database connection label is "established" when healthy', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runDashboard();
|
||||
expect(body.services.database.connection).toBe('established');
|
||||
});
|
||||
|
||||
test('database connection label is "unavailable" when down', async () => {
|
||||
db.authenticate.mockRejectedValue(new Error('refused'));
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
resolve(smtp);
|
||||
|
||||
const { body } = await runDashboard();
|
||||
expect(body.services.database.connection).toBe('unavailable');
|
||||
});
|
||||
|
||||
test('latency_ms is a non-negative number for enabled checks', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runDashboard();
|
||||
expect(typeof body.services.database.latency_ms).toBe('number');
|
||||
expect(body.services.database.latency_ms).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('timestamp is a valid ISO 8601 string', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runDashboard();
|
||||
expect(new Date(body.timestamp).toISOString()).toBe(body.timestamp);
|
||||
});
|
||||
});
|
||||
|
||||
// ── runReadiness ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe('runReadiness()', () => {
|
||||
|
||||
test('all healthy → status=healthy, HTTP 200', async () => {
|
||||
allHealthy();
|
||||
const { httpStatus, body } = await runReadiness();
|
||||
expect(httpStatus).toBe(200);
|
||||
expect(body.status).toBe('healthy');
|
||||
});
|
||||
|
||||
test('database down → HTTP 503', async () => {
|
||||
db.authenticate.mockRejectedValue(new Error('refused'));
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
resolve(smtp);
|
||||
|
||||
const { httpStatus } = await runReadiness();
|
||||
expect(httpStatus).toBe(503);
|
||||
});
|
||||
|
||||
test('S3 down → HTTP 200, degraded — non-critical stays routable', async () => {
|
||||
resolve(db.authenticate);
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
s3.mockRejectedValue(new Error('S3 storage unavailable'));
|
||||
resolve(smtp);
|
||||
|
||||
const { httpStatus, body } = await runReadiness();
|
||||
expect(httpStatus).toBe(200);
|
||||
expect(body.status).toBe('degraded');
|
||||
});
|
||||
|
||||
test('body has version, uptime, timestamp, checks, memory', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runReadiness();
|
||||
expect(body).toHaveProperty('version');
|
||||
expect(body).toHaveProperty('uptime');
|
||||
expect(body).toHaveProperty('timestamp');
|
||||
expect(body).toHaveProperty('checks');
|
||||
expect(body).toHaveProperty('memory');
|
||||
});
|
||||
|
||||
test('checks object has database, cache, storage, smtp keys', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runReadiness();
|
||||
['database', 'cache', 'storage', 'smtp'].forEach((key) => {
|
||||
expect(body.checks).toHaveProperty(key);
|
||||
});
|
||||
});
|
||||
|
||||
test('memory snapshot has heap_used_mb, heap_total_mb, rss_mb, external_mb', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runReadiness();
|
||||
expect(body.memory).toMatchObject({
|
||||
heap_used_mb: expect.any(Number),
|
||||
heap_total_mb: expect.any(Number),
|
||||
rss_mb: expect.any(Number),
|
||||
external_mb: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
test('database check entry has status=unhealthy and critical=true when DB fails', async () => {
|
||||
db.authenticate.mockRejectedValue(new Error('refused'));
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
resolve(s3);
|
||||
resolve(smtp);
|
||||
|
||||
const { body } = await runReadiness();
|
||||
expect(body.checks.database.status).toBe('unhealthy');
|
||||
expect(body.checks.database.critical).toBe(true);
|
||||
});
|
||||
|
||||
test('storage check entry has status=unhealthy and critical=false when S3 fails', async () => {
|
||||
resolve(db.authenticate);
|
||||
cache.ping.mockResolvedValue('PONG');
|
||||
s3.mockRejectedValue(new Error('S3 storage unavailable'));
|
||||
resolve(smtp);
|
||||
|
||||
const { body } = await runReadiness();
|
||||
expect(body.checks.storage.status).toBe('unhealthy');
|
||||
expect(body.checks.storage.critical).toBe(false);
|
||||
});
|
||||
|
||||
test('cache check entry has status=skipped when redis is null (CACHE_DRIVER=memory)', async () => {
|
||||
allHealthy();
|
||||
const { body } = await runReadiness();
|
||||
// cache mock returns { ping: fn } so it is enabled; but the skipped test
|
||||
// is covered when redis module exports null — we verify the shape here instead
|
||||
expect(['healthy', 'skipped']).toContain(body.checks.cache.status);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user