import type { NextFunction, Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { asyncHandler } from "./async-handler.js";
describe("asyncHandler", () => {
let mockReq: Partial<Request>;
let mockRes: Partial<Response>;
let mockNext: NextFunction;
beforeEach(() => {
mockReq = {};
mockRes = {
json: vi.fn(),
status: vi.fn().mockReturnThis(),
};
mockNext = vi.fn();
});
it("should catch promise rejections and forward to next()", async () => {
const error = new Error("Async error");
const handler = asyncHandler(async () => {
throw error;
});
handler(mockReq as Request, mockRes as Response, mockNext);
// Wait for promise to resolve
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockNext).toHaveBeenCalledWith(error);
});
it("should forward errors to next()", async () => {
const error = new Error("Test error");
const handler = asyncHandler(async () => {
throw error;
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockNext).toHaveBeenCalledWith(error);
expect(mockNext).toHaveBeenCalledTimes(1);
});
it("should allow successful execution passthrough", async () => {
const handler = asyncHandler(async (_req, res) => {
res.json({ success: true });
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockRes.json).toHaveBeenCalledWith({ success: true });
expect(mockNext).not.toHaveBeenCalled();
});
it("should handle multiple async handlers independently", async () => {
const error = new Error("Handler 1 error");
const handler1 = asyncHandler(async () => {
throw error;
});
const handler2 = asyncHandler(async (_req, res) => {
res.json({ success: true });
});
const mockNext1 = vi.fn();
const mockNext2 = vi.fn();
const mockRes1 = { json: vi.fn() };
const mockRes2 = { json: vi.fn() };
handler1(mockReq as Request, mockRes1 as Response, mockNext1);
handler2(mockReq as Request, mockRes2 as Response, mockNext2);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockNext1).toHaveBeenCalledWith(error);
expect(mockRes2.json).toHaveBeenCalledWith({ success: true });
expect(mockNext2).not.toHaveBeenCalled();
});
it("should catch synchronous errors in async functions", async () => {
const error = new Error("Sync error in async");
const handler = asyncHandler(async () => {
throw error; // Synchronous throw in async function
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockNext).toHaveBeenCalledWith(error);
});
it("should pass through all arguments to handler", async () => {
const handler = asyncHandler(async (req, res, next) => {
expect(req).toBe(mockReq);
expect(res).toBe(mockRes);
expect(next).toBe(mockNext);
res.json({ verified: true });
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockRes.json).toHaveBeenCalledWith({ verified: true });
});
it("should not call next() when handler succeeds", async () => {
const handler = asyncHandler(async (_req, res) => {
res.status(200).json({ data: "test" });
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith({ data: "test" });
expect(mockNext).not.toHaveBeenCalled();
});
it("should handle async operations that reject with non-Error", async () => {
const rejectionValue = "String rejection";
const handler = asyncHandler(async () => {
throw rejectionValue;
});
handler(mockReq as Request, mockRes as Response, mockNext);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(mockNext).toHaveBeenCalledWith(rejectionValue);
});
});