/**
* mysql-mcp - HTTP Transport Unit Tests
*
* Tests for HTTP transport functionality including CORS,
* health checks, OAuth metadata, and request routing.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { HttpTransport, createHttpTransport } from "../http.js";
import type { IncomingMessage, ServerResponse } from "node:http";
import { createServer } from "node:http";
// Mock node:http
vi.mock("node:http", () => {
const mockServer = {
listen: vi.fn((port, host, cb) => cb()),
close: vi.fn((cb) => cb && cb()),
on: vi.fn(),
};
return {
createServer: vi.fn(() => mockServer),
IncomingMessage: vi.fn(),
ServerResponse: vi.fn(),
};
});
// Mock SDK StreamableHTTPServerTransport
vi.mock("@modelcontextprotocol/sdk/server/streamableHttp.js", () => {
return {
StreamableHTTPServerTransport: vi.fn(function () {
return {
start: vi.fn().mockResolvedValue(undefined),
handleRequest: vi.fn().mockResolvedValue(undefined),
};
}),
};
});
// Mock logger to avoid console output
vi.mock("../../utils/logger.js", () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
},
}));
describe("HttpTransport", () => {
let transport: HttpTransport;
beforeEach(() => {
vi.clearAllMocks();
transport = new HttpTransport({
port: 3000,
host: "localhost",
});
});
describe("Lifecycle", () => {
it("should start server on start()", async () => {
await transport.start();
expect(createServer).toHaveBeenCalled();
const server = (createServer as any).mock.results[0].value;
expect(server.listen).toHaveBeenCalledWith(
3000,
"localhost",
expect.any(Function),
);
expect(server.on).toHaveBeenCalledWith("error", expect.any(Function));
});
it("should stop server on stop()", async () => {
await transport.start();
await transport.stop();
const server = (createServer as any).mock.results[0].value;
expect(server.close).toHaveBeenCalled();
});
it("should do nothing on stop() if not started", async () => {
const result = await transport.stop();
expect(result).toBeUndefined();
});
});
describe("Construction", () => {
it("should create transport with config", () => {
expect(transport).toBeInstanceOf(HttpTransport);
});
it("should use default host when not provided", () => {
const t = new HttpTransport({ port: 8080 });
expect(t).toBeInstanceOf(HttpTransport);
});
});
describe("setCorsHeaders()", () => {
it("should not set CORS headers when origin not in allowed list", () => {
const transportWithCors = new HttpTransport({
port: 3000,
corsOrigins: ["https://allowed.example.com"],
});
const mockReq = {
headers: { origin: "https://notallowed.example.com" },
} as IncomingMessage;
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
// Access private method via prototype
(
transportWithCors as unknown as {
setCorsHeaders: (req: IncomingMessage, res: ServerResponse) => void;
}
).setCorsHeaders(mockReq, mockRes);
expect(mockRes.setHeader).not.toHaveBeenCalled();
});
it("should set CORS headers when origin is allowed", () => {
const transportWithCors = new HttpTransport({
port: 3000,
corsOrigins: ["https://allowed.example.com"],
});
const mockReq = {
headers: { origin: "https://allowed.example.com" },
} as IncomingMessage;
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transportWithCors as unknown as {
setCorsHeaders: (req: IncomingMessage, res: ServerResponse) => void;
}
).setCorsHeaders(mockReq, mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"Access-Control-Allow-Origin",
"https://allowed.example.com",
);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS",
);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"Access-Control-Allow-Headers",
"Content-Type, Authorization",
);
});
it("should not set CORS when no origin header", () => {
const transportWithCors = new HttpTransport({
port: 3000,
corsOrigins: ["https://allowed.example.com"],
});
const mockReq = {
headers: {},
} as IncomingMessage;
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transportWithCors as unknown as {
setCorsHeaders: (req: IncomingMessage, res: ServerResponse) => void;
}
).setCorsHeaders(mockReq, mockRes);
expect(mockRes.setHeader).not.toHaveBeenCalled();
});
});
describe("handleHealthCheck()", () => {
it("should return healthy status", () => {
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
handleHealthCheck: (res: ServerResponse) => void;
}
).handleHealthCheck(mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(200, {
"Content-Type": "application/json",
});
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const response = JSON.parse(endCall as string);
expect(response).toHaveProperty("status", "healthy");
expect(response).toHaveProperty("timestamp");
});
it("should include ISO timestamp", () => {
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
handleHealthCheck: (res: ServerResponse) => void;
}
).handleHealthCheck(mockRes);
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const response = JSON.parse(endCall as string);
// Validate ISO timestamp format
expect(() => new Date(response.timestamp as string)).not.toThrow();
});
});
describe("handleProtectedResourceMetadata()", () => {
it("should return 404 when OAuth not configured", () => {
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
handleProtectedResourceMetadata: (res: ServerResponse) => void;
}
).handleProtectedResourceMetadata(mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(404);
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const response = JSON.parse(endCall as string);
expect(response).toHaveProperty("error");
});
it("should return metadata when OAuth configured", () => {
const mockResourceServer = {
getMetadata: () => ({
resource: "https://mysql-mcp.example.com",
authorization_servers: ["https://auth.example.com"],
scopes_supported: ["read", "write"],
}),
};
const transportWithOAuth = new HttpTransport({
port: 3000,
resourceServer: mockResourceServer as never,
});
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
} as unknown as ServerResponse;
(
transportWithOAuth as unknown as {
handleProtectedResourceMetadata: (res: ServerResponse) => void;
}
).handleProtectedResourceMetadata(mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(200, {
"Content-Type": "application/json",
});
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock
.calls[0][0];
const response = JSON.parse(endCall as string);
expect(response).toHaveProperty("resource");
expect(response).toHaveProperty("authorization_servers");
});
});
describe("setSecurityHeaders()", () => {
it("should set X-Content-Type-Options header", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"X-Content-Type-Options",
"nosniff",
);
});
it("should set X-Frame-Options header", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith("X-Frame-Options", "DENY");
});
it("should set X-XSS-Protection header", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"X-XSS-Protection",
"1; mode=block",
);
});
it("should set Cache-Control header", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"Cache-Control",
"no-store, no-cache, must-revalidate",
);
});
it("should set Content-Security-Policy header", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
expect(mockRes.setHeader).toHaveBeenCalledWith(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'",
);
});
it("should set all 5 security headers", () => {
const mockRes = {
setHeader: vi.fn(),
} as unknown as ServerResponse;
(
transport as unknown as {
setSecurityHeaders: (res: ServerResponse) => void;
}
).setSecurityHeaders(mockRes);
// Verify exactly 5 headers were set
expect(mockRes.setHeader).toHaveBeenCalledTimes(5);
});
});
});
describe("handleRequest()", () => {
let transport: HttpTransport;
beforeEach(() => {
transport = new HttpTransport({
port: 3000,
corsOrigins: ["https://example.com"],
});
});
it("should handle OPTIONS preflight request", async () => {
const mockReq = {
method: "OPTIONS",
url: "/mcp",
headers: { host: "localhost:3000", origin: "https://example.com" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(204);
expect(mockRes.end).toHaveBeenCalled();
});
it("should route to health check endpoint", async () => {
const mockReq = {
method: "GET",
url: "/health",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(200, {
"Content-Type": "application/json",
});
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(JSON.parse(endCall as string)).toHaveProperty("status", "healthy");
});
it("should route to OAuth metadata endpoint", async () => {
const mockReq = {
method: "GET",
url: "/.well-known/oauth-protected-resource",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
// Without OAuth configured, should return 404
expect(mockRes.writeHead).toHaveBeenCalledWith(404);
});
it("should return 404 for unknown paths", async () => {
const mockReq = {
method: "GET",
url: "/unknown-path",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(404);
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(JSON.parse(endCall as string)).toHaveProperty("error", "Not found");
});
it("should reject unauthenticated requests when OAuth is configured", async () => {
const mockTokenValidator = {
validate: vi
.fn()
.mockResolvedValue({ valid: false, error: "Token missing" }),
};
const mockResourceServer = {
getMetadata: vi.fn().mockReturnValue({ resource: "test" }),
};
const transportWithOAuth = new HttpTransport({
port: 3000,
resourceServer: mockResourceServer as never,
tokenValidator: mockTokenValidator as never,
});
const mockReq = {
method: "GET",
url: "/mcp",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transportWithOAuth as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(
401,
expect.objectContaining({
"WWW-Authenticate": "Bearer",
}),
);
});
it("should allow authenticated requests when OAuth is configured", async () => {
const mockTokenValidator = {
validate: vi.fn().mockResolvedValue({
valid: true,
claims: { sub: "user1", scopes: ["read"], exp: 0, iat: 0 },
}),
};
const mockResourceServer = {
getMetadata: vi.fn().mockReturnValue({ resource: "test" }),
};
const transportWithOAuth = new HttpTransport({
port: 3000,
resourceServer: mockResourceServer as never,
tokenValidator: mockTokenValidator as never,
});
const mockReq = {
method: "GET",
url: "/mcp",
headers: {
host: "localhost:3000",
authorization: "Bearer valid_token",
},
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
await (
transportWithOAuth as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
// After successful auth, should return 404 (no MCP handling yet)
expect(mockRes.writeHead).toHaveBeenCalledWith(404);
});
});
describe("SSE Support", () => {
it("should route to SSE endpoint", async () => {
const mockReq = {
method: "GET",
url: "/sse",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
// Spy on onConnect callback
const onConnect = vi.fn();
const transportWithCallback = createHttpTransport(
{ port: 3000 },
onConnect,
);
const { StreamableHTTPServerTransport } =
await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
await (
transportWithCallback as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(StreamableHTTPServerTransport).toHaveBeenCalled();
// Verify start called (mocked)
const mockTransportInstance = (
StreamableHTTPServerTransport as unknown as ReturnType<typeof vi.fn>
).mock.results[0].value;
expect(mockTransportInstance.start).toHaveBeenCalled();
expect(onConnect).toHaveBeenCalledWith(mockTransportInstance);
expect(mockTransportInstance.handleRequest).toHaveBeenCalledWith(
mockReq,
mockRes,
);
});
it("should route to messages endpoint", async () => {
const mockReq = {
method: "POST",
url: "/messages",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
// Setup transport with active SSE connection
const transport = new HttpTransport({ port: 3000 });
const { StreamableHTTPServerTransport } =
await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
const mockTransportInstance = new StreamableHTTPServerTransport();
// Manually set transport (private)
Object.defineProperty(transport, "transport", {
value: mockTransportInstance,
writable: true,
});
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockTransportInstance.handleRequest).toHaveBeenCalledWith(
mockReq,
mockRes,
);
});
it("should return 400 for messages without active connection", async () => {
const mockReq = {
method: "POST",
url: "/messages",
headers: { host: "localhost:3000" },
} as IncomingMessage;
const mockRes = {
writeHead: vi.fn(),
end: vi.fn(),
setHeader: vi.fn(),
} as unknown as ServerResponse;
const transport = new HttpTransport({ port: 3000 });
await (
transport as unknown as {
handleRequest: (
req: IncomingMessage,
res: ServerResponse,
) => Promise<void>;
}
).handleRequest(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(400);
const endCall = (mockRes.end as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(JSON.parse(endCall as string)).toHaveProperty(
"error",
"No active connection",
);
});
});
describe("createHttpTransport()", () => {
it("should create HttpTransport instance", () => {
const transport = createHttpTransport({ port: 8080 });
expect(transport).toBeInstanceOf(HttpTransport);
});
it("should pass config to transport", () => {
const transport = createHttpTransport({
port: 3000,
host: "0.0.0.0",
corsOrigins: ["https://example.com"],
});
expect(transport).toBeInstanceOf(HttpTransport);
});
});