/** @jest-environment node */ import type { NextApiRequest, NextApiResponse } from "next"; import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares"; import { BaseError, LangfuseNotFoundError, UnauthorizedError, } from "@langfuse/shared"; import { ClickHouseResourceError, logger, traceException, } from "@langfuse/shared/src/server"; import { createMocks } from "node-mocks-http"; import { z } from "zod/v4"; import { Prisma } from "@prisma/client"; // Mock the logger and traceException jest.mock("@langfuse/shared/src/server", () => ({ ...jest.requireActual("@langfuse/shared/src/server"), logger: { info: jest.fn(), error: jest.fn(), debug: jest.fn(), }, traceException: jest.fn(), })); describe("withMiddlewares error handling", () => { beforeEach(() => { jest.clearAllMocks(); }); describe("BaseError handling", () => { it("should handle BaseError with 4xx status code", async () => { const error = new BaseError("BadRequest", 400, "Bad Request", false); const handler = withMiddlewares({ POST: async () => { throw error; }, }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(400); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Bad Request", error: "BadRequest", }); }); it("should handle BaseError with 5xx status code and trace exception", async () => { const error = new BaseError( "ServiceUnavailable", 503, "Internal Error", true, ); const handler = withMiddlewares({ GET: async () => { throw error; }, }); const { req, res } = createMocks({ method: "GET", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(503); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Internal Error", error: "ServiceUnavailable", }); // Should trace 5xx errors expect(traceException).toHaveBeenCalledWith(error); }); }); describe("LangfuseNotFoundError handling", () => { it("should handle LangfuseNotFoundError and log as info", async () => { const error = new LangfuseNotFoundError("Resource not found"); const handler = withMiddlewares({ GET: async () => { throw error; }, }); const { req, res } = createMocks({ method: "GET", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(404); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Resource not found", error: "LangfuseNotFoundError", }); // Should log as info, not error expect(logger.info).toHaveBeenCalledWith(error); expect(logger.error).not.toHaveBeenCalled(); }); }); describe("UnauthorizedError handling", () => { it("should handle UnauthorizedError and log as info", async () => { const error = new UnauthorizedError("Invalid credentials"); const handler = withMiddlewares({ POST: async () => { throw error; }, }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(401); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Invalid credentials", error: "UnauthorizedError", }); // Should log as info, not error expect(logger.info).toHaveBeenCalledWith(error); expect(logger.error).not.toHaveBeenCalled(); }); }); describe("MethodNotAllowedError handling", () => { it("should throw MethodNotAllowedError for unsupported methods", async () => { const handler = withMiddlewares({ GET: async () => {}, // POST is not defined }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(405); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Method not allowed", error: "MethodNotAllowedError", }); }); }); describe("ClickHouseResourceError handling", () => { it("should handle ClickHouseResourceError with 400 status", async () => { const originalError = new Error("Memory limit exceeded: maximum: 10GB"); const resourceError = new ClickHouseResourceError( "MEMORY_LIMIT", originalError, ); const handler = withMiddlewares({ POST: async () => { throw resourceError; }, }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(524); const jsonData = JSON.parse(res._getData()); expect(jsonData["message"]).toBeDefined(); expect(jsonData["message"]).toContain( ClickHouseResourceError.ERROR_ADVICE_MESSAGE, ); }); }); describe("Prisma exception handling", () => { it("should handle Prisma exceptions with generic 500 error", async () => { // Create a real Prisma error const prismaError = new Prisma.PrismaClientKnownRequestError( "Unique constraint failed", { code: "P2002", clientVersion: "5.0.0" }, ); const handler = withMiddlewares({ POST: async () => { throw prismaError; }, }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(500); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Internal Server Error", error: "An unknown error occurred", }); expect(traceException).toHaveBeenCalledWith(prismaError); }); }); describe("Zod validation error handling", () => { it("should handle Zod validation errors with 400 status", async () => { const schema = z.object({ name: z.string(), age: z.number(), }); const handler = withMiddlewares({ POST: async () => { // This will throw a ZodError schema.parse({ name: "John", age: "not a number" }); }, }); const { req, res } = createMocks({ method: "POST", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(400); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Invalid request data", error: expect.arrayContaining([ expect.objectContaining({ code: "invalid_type", path: ["age"], }), ]), }); }); }); describe("Generic error handling", () => { it("should handle generic Error instances with 500 status", async () => { const error = new Error("Something went wrong"); const handler = withMiddlewares({ DELETE: async () => { throw error; }, }); const { req, res } = createMocks({ method: "DELETE", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(500); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Internal Server Error", error: "Something went wrong", }); expect(traceException).toHaveBeenCalledWith(error); }); it("should handle non-Error thrown values with 500 status", async () => { const handler = withMiddlewares({ PATCH: async () => { throw "string error"; // eslint-disable-line no-throw-literal }, }); const { req, res } = createMocks({ method: "PATCH", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(500); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Internal Server Error", error: "An unknown error occurred", }); }); it("should handle null/undefined errors with 500 status", async () => { const handler = withMiddlewares({ PUT: async () => { throw null; // eslint-disable-line no-throw-literal }, }); const { req, res } = createMocks({ method: "PUT", headers: { "x-langfuse-public-key": "test-key", }, }); await handler(req, res); expect(res._getStatusCode()).toBe(500); const jsonData = JSON.parse(res._getData()); expect(jsonData).toMatchObject({ message: "Internal Server Error", error: "An unknown error occurred", }); }); }); });