/**
* mysql-mcp - ConnectionPool Unit Tests
*
* Tests for connection pool initialization, queries, health checks,
* and lifecycle management using mocked mysql2.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { ConnectionPool } from "../ConnectionPool.js";
import type { ConnectionPoolConfig } from "../ConnectionPool.js";
import mysql from "mysql2/promise";
// Mock mysql2/promise
vi.mock("mysql2/promise", () => {
const mockConnection = {
query: vi.fn().mockResolvedValue([[{ version: "8.0.35" }], []]),
execute: vi.fn().mockResolvedValue([[{ id: 1 }], []]),
release: vi.fn(),
ping: vi.fn().mockResolvedValue(undefined),
};
const mockPool = {
getConnection: vi.fn().mockResolvedValue(mockConnection),
query: vi.fn().mockResolvedValue([[], []]),
execute: vi.fn().mockResolvedValue([[], []]),
end: vi.fn().mockResolvedValue(undefined),
};
return {
default: {
createPool: vi.fn().mockReturnValue(mockPool),
},
};
});
describe("ConnectionPool", () => {
let pool: ConnectionPool;
let config: ConnectionPoolConfig;
beforeEach(() => {
vi.clearAllMocks();
config = {
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
};
pool = new ConnectionPool(config);
});
describe("constructor", () => {
it("should create a pool instance", () => {
expect(pool).toBeInstanceOf(ConnectionPool);
});
it("should not be initialized by default", () => {
expect(pool.isInitialized()).toBe(false);
});
it("should not be closing by default", () => {
expect(pool.isClosing()).toBe(false);
});
});
describe("initialize", () => {
it("should initialize the pool", async () => {
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
});
it("should not re-initialize if already initialized", async () => {
await pool.initialize();
await pool.initialize(); // Should not throw
expect(pool.isInitialized()).toBe(true);
});
});
describe("getConnection", () => {
it("should throw if pool not initialized", async () => {
await expect(pool.getConnection()).rejects.toThrow(
"Connection pool not initialized",
);
});
it("should return a connection after initialization", async () => {
await pool.initialize();
const connection = await pool.getConnection();
expect(connection).toBeDefined();
});
it("should throw if pool is shutting down", async () => {
await pool.initialize();
await pool.shutdown();
await expect(pool.getConnection()).rejects.toThrow();
});
});
describe("query", () => {
it("should throw if pool not initialized", async () => {
await expect(pool.query("SELECT 1")).rejects.toThrow(
"Connection pool not initialized",
);
});
it("should execute queries after initialization", async () => {
await pool.initialize();
const [rows, _fields] = await pool.query("SELECT 1");
expect(rows).toBeDefined();
});
it("should track query count", async () => {
await pool.initialize();
await pool.query("SELECT 1");
await pool.query("SELECT 2");
const stats = pool.getStats();
expect(stats.totalQueries).toBe(2);
});
});
describe("execute", () => {
it("should throw if pool not initialized", async () => {
await expect(pool.execute("SELECT ?", [1])).rejects.toThrow(
"Connection pool not initialized",
);
});
it("should execute prepared statements after initialization", async () => {
await pool.initialize();
const [rows, _fields] = await pool.execute("SELECT ?", [1]);
expect(rows).toBeDefined();
});
it("should track query count for execute", async () => {
await pool.initialize();
await pool.execute("SELECT 1");
const stats = pool.getStats();
expect(stats.totalQueries).toBe(1);
});
});
describe("getStats", () => {
it("should return stats even before initialization", () => {
const stats = pool.getStats();
expect(stats).toHaveProperty("total");
expect(stats).toHaveProperty("active");
expect(stats).toHaveProperty("idle");
expect(stats).toHaveProperty("waiting");
expect(stats).toHaveProperty("totalQueries");
});
it("should track active connections", async () => {
await pool.initialize();
const _conn = await pool.getConnection();
const stats = pool.getStats();
expect(stats.active).toBe(1);
});
});
describe("releaseConnection", () => {
it("should decrease active count", async () => {
await pool.initialize();
const conn = await pool.getConnection();
expect(pool.getStats().active).toBe(1);
pool.releaseConnection(conn);
expect(pool.getStats().active).toBe(0);
});
it("should not go below 0 active connections", async () => {
await pool.initialize();
const conn = await pool.getConnection();
pool.releaseConnection(conn);
pool.releaseConnection(conn); // Release twice
expect(pool.getStats().active).toBe(0);
});
});
describe("checkHealth", () => {
it("should return unhealthy if not initialized", async () => {
const health = await pool.checkHealth();
expect(health.connected).toBe(false);
expect(health.error).toContain("not initialized");
});
it("should return healthy after initialization", async () => {
await pool.initialize();
const health = await pool.checkHealth();
expect(health.connected).toBe(true);
expect(health.latencyMs).toBeDefined();
expect(health.version).toBe("8.0.35");
});
it("should include pool stats in health check", async () => {
await pool.initialize();
const health = await pool.checkHealth();
expect(health.poolStats).toBeDefined();
});
});
describe("shutdown", () => {
it("should do nothing if not initialized", async () => {
await pool.shutdown(); // Should not throw
expect(pool.isInitialized()).toBe(false);
});
it("should shutdown the pool", async () => {
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
await pool.shutdown();
expect(pool.isClosing()).toBe(true);
});
});
describe("isInitialized", () => {
it("should return false before initialization", () => {
expect(pool.isInitialized()).toBe(false);
});
it("should return true after initialization", async () => {
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
});
});
describe("isClosing", () => {
it("should return false initially", () => {
expect(pool.isClosing()).toBe(false);
});
it("should return true after shutdown starts", async () => {
await pool.initialize();
await pool.shutdown();
expect(pool.isClosing()).toBe(true);
});
});
});
describe("ConnectionPool with SSL", () => {
it("should handle boolean SSL config", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
ssl: true,
});
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
});
it("should handle boolean SSL config false", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
ssl: false,
});
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
});
it("should handle SSL options config", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
ssl: { rejectUnauthorized: false },
});
await pool.initialize();
expect(pool.isInitialized()).toBe(true);
});
});
describe("ConnectionPool with custom pool config", () => {
it("should apply custom connection limit", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
pool: {
connectionLimit: 20,
waitForConnections: true,
queueLimit: 10,
},
});
await pool.initialize();
const stats = pool.getStats();
expect(stats.total).toBe(20);
});
});
describe("ConnectionPool Error Handling", () => {
it("should handle initialization failure", async () => {
const createPoolSpy = vi.spyOn(mysql, "createPool");
createPoolSpy.mockImplementationOnce(() => {
throw new Error("Connection refused");
});
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await expect(pool.initialize()).rejects.toThrow(
"Failed to initialize connection pool",
);
});
it("should fail if pool not initialized", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
// Skip initialize
await expect(pool.getConnection()).rejects.toThrow(
"Connection pool not initialized",
);
await expect(pool.query("SELECT 1")).rejects.toThrow(
"Connection pool not initialized",
);
await expect(pool.execute("SELECT 1")).rejects.toThrow(
"Connection pool not initialized",
);
});
it("should handle getConnection failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
// Spy on the internal pool's getConnection
// We cast to any to access the private pool property or just rely on the mock factory
const internalPool = (pool as any).pool;
vi.spyOn(internalPool, "getConnection").mockRejectedValueOnce(
new Error("Pool exhausted"),
);
await expect(pool.getConnection()).rejects.toThrow(
"Failed to get connection: Pool exhausted",
);
});
it("should handle query failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
const internalPool = (pool as any).pool;
vi.spyOn(internalPool, "query").mockRejectedValueOnce(
new Error("Query failed"),
);
await expect(pool.query("SELECT 1")).rejects.toThrow(
"Query failed: Query failed",
);
});
it("should handle execute failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
const internalPool = (pool as any).pool;
vi.spyOn(internalPool, "execute").mockRejectedValueOnce(
new Error("Execution failed"),
);
await expect(
pool.execute("UPDATE users SET name = ?", ["test"]),
).rejects.toThrow("Execute failed: Execution failed");
});
it("should fail if shutting down", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
// Force shutting down state
(pool as any).isShuttingDown = true;
await expect(pool.getConnection()).rejects.toThrow(
"Connection pool is shutting down",
);
await expect(pool.getConnection()).rejects.toThrow(
"Connection pool is shutting down",
);
});
it("should handle shutdown failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
const internalPool = (pool as any).pool;
vi.spyOn(internalPool, "end").mockRejectedValueOnce(
new Error("Forced error"),
);
await expect(pool.shutdown()).rejects.toThrow("Forced error");
});
it("should handle releaseConnection failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
const connection = await pool.getConnection();
// Mock release to throw
vi.spyOn(connection, "release").mockImplementationOnce(() => {
throw new Error("Release failed");
});
// Should not throw, but log error
pool.releaseConnection(connection);
});
it("should handle checkHealth failure", async () => {
const pool = new ConnectionPool({
host: "localhost",
port: 3306,
user: "root",
password: "root",
database: "testdb",
});
await pool.initialize();
const internalPool = (pool as any).pool;
vi.spyOn(internalPool, "getConnection").mockRejectedValueOnce(
new Error("Health check failed"),
);
const health = await pool.checkHealth();
expect(health.connected).toBe(false);
expect(health.error).toContain("Health check failed");
});
});