From 1286cd1eae23298d8c4ae5a5bc6b984070ce02f4 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:09:49 +0330 Subject: [PATCH] Import tests: backend/tests/integration/resilience Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- backend/tests/integration/resilience.test.ts | 152 +++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 backend/tests/integration/resilience.test.ts diff --git a/backend/tests/integration/resilience.test.ts b/backend/tests/integration/resilience.test.ts new file mode 100644 index 0000000..1970999 --- /dev/null +++ b/backend/tests/integration/resilience.test.ts @@ -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; + + 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(); + }); + }); +});