Import tests: backend/tests/integration/resilience
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user