Skip to main content
Glama
connection-manager.test.ts21.6 kB
/** * DatabaseConnectionManager Tests * * Tests for PostgreSQL connection management with pooling, transactions, and error handling. * Following TDD principles - these tests define expected behavior before implementation. */ import { Pool } from "pg"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // Import implementation that will be completed in task 1.1.2 import { DatabaseConnectionManager, type DatabaseConfig, } from "../../../database/connection-manager"; // Mock pg module vi.mock("pg", () => { return { Pool: vi.fn(), }; }); // Helper to create fresh mocks for each test type MockClient = { query: ReturnType<typeof vi.fn>; release: ReturnType<typeof vi.fn>; }; type MockPool = { connect: ReturnType<typeof vi.fn>; end: ReturnType<typeof vi.fn>; query: ReturnType<typeof vi.fn>; totalCount: number; idleCount: number; waitingCount: number; }; function createMockPoolAndClient(): { mockPool: MockPool; mockClient: MockClient } { const mockClient: MockClient = { query: vi.fn().mockResolvedValue({ rows: [{ result: 1 }] }), release: vi.fn(), }; const mockPool: MockPool = { connect: vi.fn().mockResolvedValue(mockClient), end: vi.fn().mockResolvedValue(undefined), query: vi.fn().mockResolvedValue({ rows: [{ result: 1 }] }), totalCount: 20, idleCount: 15, waitingCount: 0, }; return { mockPool, mockClient }; } describe("DatabaseConnectionManager", () => { let manager: DatabaseConnectionManager; let config: DatabaseConfig; let mockPool: MockPool; beforeEach(() => { // Create fresh mocks for each test const mocks = createMockPoolAndClient(); mockPool = mocks.mockPool; // Setup Pool constructor to return our mock vi.mocked(Pool).mockImplementation(() => mockPool as unknown as Pool); config = { host: "localhost", port: 5432, database: "test_db", user: "test_user", password: "test_pass", poolSize: 20, connectionTimeout: 5000, idleTimeout: 30000, }; manager = new DatabaseConnectionManager(config); }); afterEach(async () => { // Cleanup: disconnect if connected if (manager.pool) { try { await manager.disconnect(); } catch { // Ignore errors during cleanup } } vi.clearAllMocks(); }); describe("Connection Pool Creation", () => { it("should create connection pool with valid configuration", async () => { await manager.connect(); expect(manager.pool).toBeDefined(); expect(manager.config.poolSize).toBe(20); expect(manager.config.connectionTimeout).toBe(5000); expect(manager.config.idleTimeout).toBe(30000); }); it("should use default values for optional configuration", async () => { const minimalConfig = { host: "localhost", port: 5432, database: "test_db", user: "test_user", password: "test_pass", }; const managerWithDefaults = new DatabaseConnectionManager(minimalConfig as DatabaseConfig); await managerWithDefaults.connect(); // Defaults should be applied expect(managerWithDefaults.config.poolSize).toBe(20); expect(managerWithDefaults.config.connectionTimeout).toBe(5000); expect(managerWithDefaults.config.idleTimeout).toBe(30000); await managerWithDefaults.disconnect(); }); it("should establish connection within 5 seconds (Requirement 1.1)", async () => { const startTime = Date.now(); await manager.connect(); const duration = Date.now() - startTime; expect(duration).toBeLessThan(5000); expect(manager.pool).toBeDefined(); }); it("should not create duplicate pools on multiple connect calls", async () => { await manager.connect(); const firstPool = manager.pool; await manager.connect(); const secondPool = manager.pool; expect(firstPool).toBe(secondPool); }); it("should validate configuration before connecting", async () => { const invalidConfig = { host: "", port: -1, database: "", user: "", password: "", poolSize: 0, connectionTimeout: -1, idleTimeout: -1, }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow(); }); }); describe("Connection Acquisition and Release", () => { it("should acquire connection from pool", async () => { await manager.connect(); const client = await manager.getConnection(); expect(client).toBeDefined(); expect(client.query).toBeDefined(); expect(client.release).toBeDefined(); manager.releaseConnection(client); }); it("should release connection back to pool", async () => { await manager.connect(); const client = await manager.getConnection(); manager.releaseConnection(client); expect(client.release).toHaveBeenCalled(); }); it("should handle connection timeout", async () => { await manager.connect(); // Mock pool to simulate timeout const mockPool = manager.pool as any; mockPool.connect = vi.fn().mockRejectedValue(new Error("Connection timeout")); await expect(manager.getConnection()).rejects.toThrow("Connection timeout"); }); it("should throw error when getting connection before connect", async () => { await expect(manager.getConnection()).rejects.toThrow(); }); it("should handle pool exhaustion gracefully", async () => { await manager.connect(); // Mock pool to simulate exhaustion const mockPool = manager.pool as any; mockPool.connect = vi .fn() .mockRejectedValue(new Error("Pool exhausted - all connections in use")); await expect(manager.getConnection()).rejects.toThrow("Pool exhausted"); }); }); describe("Transaction Management", () => { it("should begin transaction", async () => { await manager.connect(); const client = await manager.beginTransaction(); expect(client).toBeDefined(); expect(client.query).toHaveBeenCalledWith("BEGIN"); }); it("should commit transaction", async () => { await manager.connect(); const client = await manager.beginTransaction(); await manager.commitTransaction(client); expect(client.query).toHaveBeenCalledWith("COMMIT"); expect(client.release).toHaveBeenCalled(); }); it("should rollback transaction", async () => { await manager.connect(); const client = await manager.beginTransaction(); await manager.rollbackTransaction(client); expect(client.query).toHaveBeenCalledWith("ROLLBACK"); expect(client.release).toHaveBeenCalled(); }); it("should rollback transaction on error", async () => { await manager.connect(); const client = await manager.beginTransaction(); // Simulate error during transaction (client.query as any).mockRejectedValueOnce(new Error("Query failed")); await expect(client.query("INVALID SQL")).rejects.toThrow("Query failed"); // Should still be able to rollback await manager.rollbackTransaction(client); expect(client.query).toHaveBeenCalledWith("ROLLBACK"); }); it("should handle nested transaction attempts", async () => { await manager.connect(); const client1 = await manager.beginTransaction(); const client2 = await manager.beginTransaction(); // Should get different clients for different transactions expect(client1).toBeDefined(); expect(client2).toBeDefined(); await manager.commitTransaction(client1); await manager.commitTransaction(client2); }); it("should release client even if commit fails", async () => { await manager.connect(); const client = await manager.beginTransaction(); // Mock commit failure (client.query as any).mockRejectedValueOnce(new Error("Commit failed")); await expect(manager.commitTransaction(client)).rejects.toThrow("Commit failed"); expect(client.release).toHaveBeenCalled(); }); it("should release client even if rollback fails", async () => { await manager.connect(); const client = await manager.beginTransaction(); // Mock rollback failure (client.query as any).mockRejectedValueOnce(new Error("Rollback failed")); await expect(manager.rollbackTransaction(client)).rejects.toThrow("Rollback failed"); expect(client.release).toHaveBeenCalled(); }); }); describe("Health Check", () => { it("should return true when database is healthy", async () => { await manager.connect(); const isHealthy = await manager.healthCheck(); expect(isHealthy).toBe(true); expect(manager.pool?.query).toHaveBeenCalledWith("SELECT 1"); }); it("should return false when database is unhealthy", async () => { await manager.connect(); // Mock query failure const mockPool = manager.pool as any; mockPool.query = vi.fn().mockRejectedValue(new Error("Connection lost")); const isHealthy = await manager.healthCheck(); expect(isHealthy).toBe(false); }); it("should complete health check within 1 second", async () => { await manager.connect(); const startTime = Date.now(); await manager.healthCheck(); const duration = Date.now() - startTime; expect(duration).toBeLessThan(1000); }); it("should return false when not connected", async () => { const isHealthy = await manager.healthCheck(); expect(isHealthy).toBe(false); }); it("should handle health check timeout", async () => { await manager.connect(); // Mock slow query const mockPool = manager.pool as any; mockPool.query = vi.fn().mockImplementation( () => new Promise((resolve) => { setTimeout(() => resolve({ rows: [{ result: 1 }] }), 2000); }) ); const startTime = Date.now(); const isHealthy = await manager.healthCheck(); const duration = Date.now() - startTime; // Should timeout and return false expect(duration).toBeLessThan(1500); expect(isHealthy).toBe(false); }); }); describe("Pool Statistics", () => { it("should return pool statistics", async () => { await manager.connect(); const stats = manager.getPoolStats(); expect(stats).toBeDefined(); expect(stats.totalConnections).toBeGreaterThanOrEqual(0); expect(stats.idleConnections).toBeGreaterThanOrEqual(0); expect(stats.activeConnections).toBeGreaterThanOrEqual(0); expect(stats.waitingClients).toBeGreaterThanOrEqual(0); }); it("should track active connections correctly", async () => { await manager.connect(); const statsBefore = manager.getPoolStats(); const client = await manager.getConnection(); const statsDuring = manager.getPoolStats(); manager.releaseConnection(client); const statsAfter = manager.getPoolStats(); expect(statsDuring.activeConnections).toBeGreaterThanOrEqual(statsBefore.activeConnections); expect(statsAfter.idleConnections).toBeGreaterThanOrEqual(statsDuring.idleConnections); }); it("should track waiting clients when pool is exhausted", async () => { await manager.connect(); const stats = manager.getPoolStats(); expect(stats.waitingClients).toBe(0); }); it("should calculate active connections correctly", async () => { await manager.connect(); const stats = manager.getPoolStats(); // active = total - idle expect(stats.activeConnections).toBe(stats.totalConnections - stats.idleConnections); }); it("should throw error when getting stats before connect", () => { expect(() => manager.getPoolStats()).toThrow(); }); }); describe("Error Handling", () => { it("should handle connection failure gracefully", async () => { // Mock Pool constructor to throw on all attempts vi.mocked(Pool).mockImplementation(() => { throw new Error("Connection refused"); }); const failingManager = new DatabaseConnectionManager(config); await expect(failingManager.connect()).rejects.toThrow("Connection refused"); }); it("should handle query errors", async () => { await manager.connect(); const client = await manager.getConnection(); // Mock query failure (client.query as any).mockRejectedValueOnce(new Error("Query syntax error")); await expect(client.query("INVALID SQL")).rejects.toThrow("Query syntax error"); manager.releaseConnection(client); }); it("should retry connection on transient failures", async () => { let attempts = 0; vi.mocked(Pool).mockImplementationOnce(() => { attempts++; if (attempts < 3) { throw new Error("Transient failure"); } return { connect: vi.fn().mockResolvedValue({ query: vi.fn(), release: vi.fn(), }), end: vi.fn().mockResolvedValue(undefined), query: vi.fn().mockResolvedValue({ rows: [{ result: 1 }] }), totalCount: 20, idleCount: 15, waitingCount: 0, } as any; }); // Should eventually succeed after retries await manager.connect(); expect(manager.pool).toBeDefined(); }); it("should handle disconnect when not connected", async () => { // Should not throw await expect(manager.disconnect()).resolves.not.toThrow(); }); it("should handle disconnect errors gracefully", async () => { await manager.connect(); // Mock end failure const mockPool = manager.pool as any; mockPool.end = vi.fn().mockRejectedValue(new Error("Disconnect failed")); // Should handle error gracefully await expect(manager.disconnect()).rejects.toThrow("Disconnect failed"); }); it("should clean up resources on disconnect", async () => { await manager.connect(); const pool = manager.pool; await manager.disconnect(); expect(pool?.end).toHaveBeenCalled(); expect(manager.pool).toBeNull(); }); }); describe("Reconnection Logic", () => { it("should automatically reconnect on connection loss", async () => { await manager.connect(); // Simulate connection loss manager.pool = null; // Should reconnect automatically const client = await manager.getConnection(); expect(client).toBeDefined(); expect(manager.pool).toBeDefined(); manager.releaseConnection(client); }); it("should use exponential backoff for retries", async () => { const delays: number[] = []; let attempts = 0; vi.mocked(Pool).mockImplementation(() => { attempts++; if (attempts < 3) { // Fail first 2 attempts, succeed on 3rd const delay = Math.min(1000 * Math.pow(2, attempts - 1), 10000); delays.push(delay); throw new Error("Connection failed"); } return { connect: vi.fn().mockResolvedValue({ query: vi.fn(), release: vi.fn(), }), end: vi.fn().mockResolvedValue(undefined), query: vi.fn().mockResolvedValue({ rows: [{ result: 1 }] }), totalCount: 20, idleCount: 15, waitingCount: 0, } as any; }); await manager.connect(); // Verify exponential backoff pattern - should have retried at least once expect(delays.length).toBeGreaterThan(0); expect(attempts).toBe(3); // Should succeed on 3rd attempt }); it("should give up after max retry attempts", async () => { vi.mocked(Pool).mockImplementation(() => { throw new Error("Persistent connection failure"); }); const failingManager = new DatabaseConnectionManager(config); await expect(failingManager.connect()).rejects.toThrow(); }); it("should handle pool cleanup errors during retry", async () => { let attempts = 0; vi.mocked(Pool).mockImplementation(() => { attempts++; const mockPool = { connect: vi.fn().mockResolvedValue({ query: vi.fn(), release: vi.fn(), }), end: vi.fn().mockRejectedValue(new Error("Cleanup failed")), query: vi.fn().mockRejectedValue(new Error("Connection test failed")), totalCount: 20, idleCount: 15, waitingCount: 0, }; if (attempts < 3) { return mockPool as any; } // Succeed on 3rd attempt return { ...mockPool, query: vi.fn().mockResolvedValue({ rows: [{ result: 1 }] }), } as any; }); // Should eventually succeed despite cleanup errors await manager.connect(); expect(manager.pool).toBeDefined(); }); it("should throw error when reconnection fails after connection loss", async () => { await manager.connect(); // Simulate connection loss manager.pool = null; // Mock Pool to fail on reconnection vi.mocked(Pool).mockImplementation(() => { throw new Error("Reconnection failed"); }); // Should throw error when reconnection fails await expect(manager.getConnection()).rejects.toThrow(); }); it("should handle edge case where pool is null after successful connect", async () => { await manager.connect(); // Simulate connection loss manager.pool = null; // Mock connect to succeed but somehow leave pool null (defensive code test) const originalConnect = manager.connect.bind(manager); manager.connect = vi.fn().mockImplementation(async () => { // Simulate connect succeeding but not setting pool (edge case) // This tests the defensive check at lines 176-177 return Promise.resolve(); }); // Should throw the defensive error await expect(manager.getConnection()).rejects.toThrow( "Failed to establish database connection" ); // Restore original connect manager.connect = originalConnect; }); }); describe("Configuration Validation", () => { it("should reject invalid host", async () => { const invalidConfig = { ...config, host: "" }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("host is required"); }); it("should reject whitespace-only host", async () => { const invalidConfig = { ...config, host: " " }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("host is required"); }); it("should reject invalid port", async () => { const invalidConfig = { ...config, port: -1 }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("port must be between"); }); it("should reject port above 65535", async () => { const invalidConfig = { ...config, port: 70000 }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("port must be between"); }); it("should reject invalid database", async () => { const invalidConfig = { ...config, database: "" }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("database is required"); }); it("should reject whitespace-only database", async () => { const invalidConfig = { ...config, database: " " }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("database is required"); }); it("should reject invalid user", async () => { const invalidConfig = { ...config, user: "" }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("user is required"); }); it("should reject whitespace-only user", async () => { const invalidConfig = { ...config, user: " " }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("user is required"); }); it("should reject invalid pool size", async () => { const invalidConfig = { ...config, poolSize: 0 }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("poolSize must be greater than 0"); }); it("should reject invalid connection timeout", async () => { const invalidConfig = { ...config, connectionTimeout: -1 }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow( "connectionTimeout must be non-negative" ); }); it("should reject invalid idle timeout", async () => { const invalidConfig = { ...config, idleTimeout: -1 }; const invalidManager = new DatabaseConnectionManager(invalidConfig); await expect(invalidManager.connect()).rejects.toThrow("idleTimeout must be non-negative"); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server