Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
153 lines
4.5 KiB
TypeScript
153 lines
4.5 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { execSync } from 'child_process';
|
|
import Redis from 'ioredis';
|
|
import { buildApp } from '../../src/app.js';
|
|
|
|
describe('System Resilience & Database Connections', () => {
|
|
let app: ReturnType<typeof buildApp>;
|
|
|
|
beforeAll(async () => {
|
|
app = buildApp();
|
|
await app.ready();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
// Safely ensure both services are running after tests finish
|
|
try {
|
|
execSync('systemctl start postgresql@18-main.service');
|
|
} catch {}
|
|
try {
|
|
execSync('systemctl start redis-server.service');
|
|
} catch {}
|
|
});
|
|
|
|
describe('PostgreSQL Connection Resilience', () => {
|
|
it('should report database: connected when PostgreSQL is up', async () => {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/health',
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.status).toBe('ok');
|
|
expect(body.database).toBe('connected');
|
|
});
|
|
|
|
it('should gracefully handle PostgreSQL going down', async () => {
|
|
// Stop PostgreSQL service
|
|
execSync('systemctl stop postgresql@18-main.service');
|
|
|
|
// Request health check (should return 500 error status and disconnected database)
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/health',
|
|
});
|
|
expect(res.statusCode).toBe(500);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.status).toBe('error');
|
|
expect(body.database).toBe('disconnected');
|
|
expect(body.message).toBeDefined();
|
|
});
|
|
|
|
it('should automatically reconnect when PostgreSQL comes back up', async () => {
|
|
// Start PostgreSQL service
|
|
execSync('systemctl start postgresql@18-main.service');
|
|
|
|
// Wait a moment for PostgreSQL to start accepting connections
|
|
let retries = 5;
|
|
let connected = false;
|
|
while (retries > 0 && !connected) {
|
|
try {
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/health',
|
|
});
|
|
const body = JSON.parse(res.body);
|
|
if (res.statusCode === 200 && body.database === 'connected') {
|
|
connected = true;
|
|
}
|
|
} catch {}
|
|
if (!connected) {
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
retries--;
|
|
}
|
|
}
|
|
|
|
expect(connected).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Redis Connection Resilience', () => {
|
|
let redis: Redis;
|
|
|
|
beforeAll(() => {
|
|
redis = new Redis('redis://127.0.0.1:6379', {
|
|
maxRetriesPerRequest: 1,
|
|
retryStrategy: () => 100, // retry quickly
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await redis.quit();
|
|
});
|
|
|
|
it('should ping Redis successfully when it is up', async () => {
|
|
const pong = await redis.ping();
|
|
expect(pong).toBe('PONG');
|
|
});
|
|
|
|
it('should fail gracefully when Redis goes down', async () => {
|
|
// Stop Redis service
|
|
execSync('systemctl stop redis-server.service');
|
|
|
|
// Attempt ping - should fail / throw
|
|
await expect(redis.ping()).rejects.toThrow();
|
|
});
|
|
|
|
it('should automatically reconnect when Redis comes back up', async () => {
|
|
// Start Redis service
|
|
execSync('systemctl start redis-server.service');
|
|
|
|
// Wait and ping - should succeed again
|
|
let retries = 5;
|
|
let pong = '';
|
|
while (retries > 0 && pong !== 'PONG') {
|
|
try {
|
|
pong = await redis.ping();
|
|
} catch {}
|
|
if (pong !== 'PONG') {
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
retries--;
|
|
}
|
|
}
|
|
expect(pong).toBe('PONG');
|
|
});
|
|
});
|
|
|
|
describe('Global Error Handler & Config Validator', () => {
|
|
it('should categorize unhandled database/connection errors using the global error handler', async () => {
|
|
const testApp = buildApp();
|
|
// Register a test route that simulates an unhandled database error BEFORE ready()
|
|
testApp.get('/test-unhandled-db-error', async () => {
|
|
throw new Error('PrismaClientKnownRequestError: Can\'t reach database server');
|
|
});
|
|
|
|
await testApp.ready();
|
|
|
|
const res = await testApp.inject({
|
|
method: 'GET',
|
|
url: '/test-unhandled-db-error',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(500);
|
|
const body = JSON.parse(res.body);
|
|
expect(body.error).toBeDefined();
|
|
expect(body.error.code).toBe('NETWORK_ERROR');
|
|
expect(body.error.message).toBe('An internal control plane error occurred.');
|
|
|
|
await testApp.close();
|
|
});
|
|
});
|
|
});
|