/** * Unit tests for src/db/pool.ts * * Covers: * - connectWithRetry succeeds on first attempt * - connectWithRetry retries on failure and succeeds on a later attempt * - connectWithRetry calls process.exit(1) after exhausting all attempts * - Warn is logged for each non-final failed attempt; fatal for the last */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { Logger } from 'pino'; import type { Pool, PoolClient } from 'pg'; import { connectWithRetry } from '../../src/db/pool.js'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeSilentLogger(): Logger { return { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn(), child: vi.fn().mockReturnThis(), trace: vi.fn(), level: 'silent', silent: vi.fn(), } as unknown as Logger; } /** * Creates a mock pg.Pool whose connect() resolves or rejects according to * the `connectResults` sequence. Each call consumes the next entry. */ function makeMockPool(connectResults: Array<'ok' | Error>): { pool: Pool; connectCallCount: () => number; } { let callIndex = 0; const clientQuery = vi.fn().mockResolvedValue({ rows: [] }); const clientRelease = vi.fn(); const mockClient = { query: clientQuery, release: clientRelease }; const connect = vi.fn(async () => { const result = connectResults[callIndex++]; if (result === 'ok') { return mockClient as unknown as PoolClient; } throw result; }); return { pool: { connect } as unknown as Pool, connectCallCount: () => callIndex, }; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('connectWithRetry', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); it('succeeds on first attempt without retrying', async () => { const { pool, connectCallCount } = makeMockPool(['ok']); const logger = makeSilentLogger(); await connectWithRetry(pool, logger, 3); expect(connectCallCount()).toBe(1); expect(logger.info).toHaveBeenCalledWith({ attempt: 1 }, 'Postgres connected'); expect(logger.warn).not.toHaveBeenCalled(); }); it('retries on failure and succeeds on the second attempt', async () => { const { pool, connectCallCount } = makeMockPool([new Error('ECONNREFUSED'), 'ok']); const logger = makeSilentLogger(); const promise = connectWithRetry(pool, logger, 2); // Advance timers to fire the backoff setTimeout (200ms * 2^0 = 200ms) await vi.runAllTimersAsync(); await promise; expect(connectCallCount()).toBe(2); expect(logger.warn).toHaveBeenCalledOnce(); expect(logger.info).toHaveBeenCalledWith({ attempt: 2 }, 'Postgres connected'); }); it('calls process.exit(1) after exhausting all attempts — maxAttempts=1', async () => { // Use maxAttempts=1 to skip backoff timers entirely, avoiding timer-related // unhandled rejection noise in the test suite. const exitSpy = vi.spyOn(process, 'exit').mockImplementation((_code) => { throw new Error('process.exit called'); }); const { pool } = makeMockPool([new Error('ECONNREFUSED')]); const logger = makeSilentLogger(); await expect(connectWithRetry(pool, logger, 1)).rejects.toThrow('process.exit called'); expect(exitSpy).toHaveBeenCalledWith(1); expect(logger.fatal).toHaveBeenCalledOnce(); // With maxAttempts=1, no retries → no warn expect(logger.warn).not.toHaveBeenCalled(); }); it('logs warn for non-final failed attempts', async () => { // maxAttempts=2: attempt 1 fails (warn), attempt 2 succeeds. // This avoids the unhandled-rejection noise that occurs when process.exit // throws inside an async function that has a pending backoff timer. const { pool } = makeMockPool([new Error('fail 1'), 'ok']); const logger = makeSilentLogger(); const promise = connectWithRetry(pool, logger, 2); await vi.runAllTimersAsync(); await promise; expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.fatal).not.toHaveBeenCalled(); expect(logger.info).toHaveBeenCalledWith({ attempt: 2 }, 'Postgres connected'); }); });