import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from "vitest";
import { main } from "../../cli.js";
import { createServer } from "../../server/McpServer.js";
import { MySQLAdapter } from "../../adapters/mysql/MySQLAdapter.js";
// Mock dependencies
vi.mock("../../server/McpServer.js");
vi.mock("../../adapters/mysql/MySQLAdapter.js");
vi.mock("../args.js", () => ({
parseArgs: vi.fn(() => ({
config: { name: "test-server", version: "1.0.0" },
databases: [],
oauth: undefined,
shouldExit: false,
})),
}));
// Mock process methods
const originalExit = process.exit;
describe("CLI Main", () => {
let mockServer: any;
let mockAdapter: any;
let mockExit: any;
let mockConsoleError: any;
let mockProcessOn: any;
// Custom error to simulate process.exit
class ExitError extends Error {
constructor(public code: number) {
super(`Process exited with code ${code}`);
}
}
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Setup mock server
mockServer = {
start: vi.fn().mockResolvedValue(undefined),
stop: vi.fn().mockResolvedValue(undefined),
registerAdapter: vi.fn(),
};
(createServer as unknown as Mock).mockReturnValue(mockServer);
// Mock MySQLAdapter
mockAdapter = {
connect: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn().mockResolvedValue(undefined),
getCapabilities: vi.fn().mockReturnValue({}),
isConnected: vi.fn().mockReturnValue(true),
};
(MySQLAdapter as unknown as Mock).mockImplementation(function () {
return mockAdapter;
});
// Mock process.exit
mockExit = vi.fn().mockImplementation((code) => {
throw new ExitError(code);
});
Object.defineProperty(process, "exit", { value: mockExit });
// Mock console.error
mockConsoleError = vi.fn();
console.error = mockConsoleError;
// Mock process.on
mockProcessOn = vi.fn();
process.on = mockProcessOn;
});
afterEach(() => {
if (originalExit) {
Object.defineProperty(process, "exit", { value: originalExit });
}
vi.restoreAllMocks();
});
it("should exit if shouldExit is true", async () => {
await expect(
main({
config: {},
databases: [],
oauth: undefined,
shouldExit: true,
}),
).rejects.toThrow(/Process exited with code 0/);
expect(mockExit).toHaveBeenCalledWith(0);
expect(createServer).not.toHaveBeenCalled();
});
it("should exit with error if no databases specified", async () => {
await expect(
main({
config: {},
databases: [],
oauth: undefined,
}),
).rejects.toThrow(/Process exited with code 1/);
expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining("Error: No database connection specified"),
);
expect(mockExit).toHaveBeenCalledWith(1);
expect(createServer).not.toHaveBeenCalled();
});
it("should start server with valid configuration", async () => {
const dbConfig = {
type: "mysql" as const,
username: "root",
password: "password",
database: "test_db",
};
await main({
config: { name: "test-server" },
databases: [dbConfig],
oauth: undefined,
});
expect(createServer).toHaveBeenCalledWith(
expect.objectContaining({
name: "test-server",
databases: [dbConfig],
}),
);
expect(MySQLAdapter).toHaveBeenCalled();
expect(mockAdapter.connect).toHaveBeenCalledWith(dbConfig);
expect(mockServer.registerAdapter).toHaveBeenCalledWith(
mockAdapter,
"mysql:test_db",
);
expect(mockServer.start).toHaveBeenCalled();
expect(mockExit).not.toHaveBeenCalled();
});
it("should log OAuth status if enabled", async () => {
const dbConfig = { type: "mysql" as const };
const oauthConfig = {
enabled: true,
issuer: "http://test",
audience: "test",
};
await main({
config: { name: "test", version: "1.0.0" },
databases: [dbConfig],
oauth: oauthConfig,
});
expect(mockConsoleError).toHaveBeenCalledWith(
"OAuth authentication enabled",
);
});
it("should handle adapter connection errors", async () => {
const dbConfig = { type: "mysql" as const };
const error = new Error("Connection failed");
mockAdapter.connect.mockRejectedValue(error);
await expect(
main({
config: {},
databases: [dbConfig],
oauth: undefined,
}),
).rejects.toThrow(/Process exited with code 1/);
expect(mockConsoleError).toHaveBeenCalledWith("Fatal error:", error);
expect(mockExit).toHaveBeenCalledWith(1);
});
it("should register signal handlers for graceful shutdown", async () => {
const dbConfig = { type: "mysql" as const };
await main({
config: {},
databases: [dbConfig],
oauth: undefined,
});
expect(mockProcessOn).toHaveBeenCalledWith("SIGINT", expect.any(Function));
expect(mockProcessOn).toHaveBeenCalledWith("SIGTERM", expect.any(Function));
});
it("should handle graceful shutdown correctly", async () => {
const dbConfig = { type: "mysql" as const };
await main({
config: {},
databases: [dbConfig],
oauth: undefined,
});
// Get the shutdown handler
const shutdownHandler = mockProcessOn.mock.calls.find(
(call: any[]) => call[0] === "SIGINT",
)[1];
// Override mockExit to not throw for this test to avoid Unhandled Rejection in the void wrapper
mockExit.mockImplementation(() => {});
// Execute shutdown
shutdownHandler();
// Wait for async shutdown to loop
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining("Shutting down"),
);
expect(mockServer.stop).toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(0);
});
});