import type { NextFunction, Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { authMiddleware } from "./auth.js";
describe("authMiddleware", () => {
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
let mockNext: NextFunction;
let jsonSpy: ReturnType<typeof vi.fn>;
let statusSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
// Reset environment variable before each test
delete process.env.SYNAPSE_API_KEY;
jsonSpy = vi.fn();
statusSpy = vi.fn().mockReturnValue({ json: jsonSpy });
mockReq = {
headers: {},
method: "POST",
path: "/mcp",
};
mockRes = {
status: statusSpy,
json: jsonSpy,
};
mockNext = vi.fn();
});
describe("when API key is not configured", () => {
it("should allow request to proceed without API key", () => {
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(mockNext).toHaveBeenCalledOnce();
expect(statusSpy).not.toHaveBeenCalled();
});
});
describe("when API key is configured", () => {
beforeEach(() => {
process.env.SYNAPSE_API_KEY = "test-secret-key-12345";
});
it("should reject request without API key header", () => {
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(statusSpy).toHaveBeenCalledWith(401);
expect(jsonSpy).toHaveBeenCalledWith({
error: "Unauthorized",
message: "Missing or invalid X-API-Key header",
});
expect(mockNext).not.toHaveBeenCalled();
});
it("should reject request with incorrect API key", () => {
mockReq.headers = { "x-api-key": "wrong-key" };
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(statusSpy).toHaveBeenCalledWith(401);
expect(jsonSpy).toHaveBeenCalledWith({
error: "Unauthorized",
message: "Missing or invalid X-API-Key header",
});
expect(mockNext).not.toHaveBeenCalled();
});
it("should accept request with correct API key", () => {
mockReq.headers = { "x-api-key": "test-secret-key-12345" };
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(mockNext).toHaveBeenCalledOnce();
expect(statusSpy).not.toHaveBeenCalled();
});
it("should handle case-insensitive header name (Express lowercases headers)", () => {
// Express automatically lowercases all header names, so "X-API-Key" becomes "x-api-key"
mockReq.headers = { "x-api-key": "test-secret-key-12345" };
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(mockNext).toHaveBeenCalledOnce();
expect(statusSpy).not.toHaveBeenCalled();
});
it("should use timing-safe comparison to prevent timing attacks", () => {
// This test verifies the implementation uses crypto.timingSafeEqual
// by checking that the function doesn't throw on comparison
mockReq.headers = { "x-api-key": "test-secret-key-12345" };
expect(() => {
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
}).not.toThrow();
expect(mockNext).toHaveBeenCalledOnce();
});
it("should reject empty API key header", () => {
mockReq.headers = { "x-api-key": "" };
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(statusSpy).toHaveBeenCalledWith(401);
expect(mockNext).not.toHaveBeenCalled();
});
it("should reject whitespace-only API key", () => {
mockReq.headers = { "x-api-key": " " };
authMiddleware(mockReq as Request, mockRes as Response, mockNext);
expect(statusSpy).toHaveBeenCalledWith(401);
expect(mockNext).not.toHaveBeenCalled();
});
});
});