From ab89085a0bf6b0c0573af355a1c2ff61cceca797 Mon Sep 17 00:00:00 2001 From: OpenClaw Subagent Date: Fri, 20 Mar 2026 17:07:56 +0100 Subject: [PATCH] chore: update marked 17.0.5, add global error handler tests (TDD) --- package-lock.json | 8 +- package.json | 2 +- src/__tests__/global-error-handler.test.ts | 125 +++++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/global-error-handler.test.ts diff --git a/package-lock.json b/package-lock.json index bb40b1b..4cfdcb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "express": "^5.1.0", "express-rate-limit": "^8.3.1", "helmet": "^8.1.0", - "marked": "^17.0.4", + "marked": "^17.0.5", "nanoid": "^5.1.6", "nodemailer": "^8.0.2", "pg": "^8.20.0", @@ -3464,9 +3464,9 @@ } }, "node_modules/marked": { - "version": "17.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", - "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "version": "17.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz", + "integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==", "license": "MIT", "bin": { "marked": "bin/marked.js" diff --git a/package.json b/package.json index 78a4fe4..80a56de 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "express": "^5.1.0", "express-rate-limit": "^8.3.1", "helmet": "^8.1.0", - "marked": "^17.0.4", + "marked": "^17.0.5", "nanoid": "^5.1.6", "nodemailer": "^8.0.2", "pg": "^8.20.0", diff --git a/src/__tests__/global-error-handler.test.ts b/src/__tests__/global-error-handler.test.ts new file mode 100644 index 0000000..386eee5 --- /dev/null +++ b/src/__tests__/global-error-handler.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import express from "express"; +import request from "supertest"; + +let app: express.Express; + +beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + + // Create a minimal test app that mimics the structure of the main app + app = express(); + + // Add request ID middleware (used by error handler) + app.use((req, _res, next) => { + req.requestId = "test-request-id"; + next(); + }); + + // Add JSON parsing middleware + app.use(express.json({ limit: "500kb" })); + + // Add test routes that can throw errors + app.post("/v1/convert/html", (_req, _res, next) => { + const err = new Error("Test API error"); + next(err); + }); + + app.get("/v1/test-error", (_req, _res, next) => { + const err = new Error("Test V1 error"); + next(err); + }); + + app.get("/health/test-error", (_req, _res, next) => { + const err = new Error("Test health error"); + next(err); + }); + + app.get("/non-api/test-error", (_req, _res, next) => { + const err = new Error("Test non-API error"); + next(err); + }); + + // Import and add the global error handler from index.ts + // We need to copy the exact error handler logic + app.use((err: unknown, req: express.Request, res: express.Response, _next: express.NextFunction) => { + const reqId = req.requestId || "unknown"; + + // Check if this is a JSON parse error from express.json() + if (err instanceof SyntaxError && 'status' in err && (err as Record).status === 400 && 'body' in err) { + if (!res.headersSent) { + res.status(400).json({ error: "Invalid JSON in request body" }); + } + return; + } + + if (!res.headersSent) { + const isApi = req.path.startsWith("/v1/") || req.path.startsWith("/health"); + if (isApi) { + res.status(500).json({ error: "Internal server error" }); + } else { + res.status(500).send("Internal server error"); + } + } + }); +}); + +describe("global error handler", () => { + it("returns 400 JSON response for invalid JSON body", async () => { + const response = await request(app) + .post("/v1/convert/html") + .set("Content-Type", "application/json") + .send("{ invalid json content") + .expect(400); + + expect(response.body).toEqual({ + error: "Invalid JSON in request body" + }); + expect(response.headers["content-type"]).toMatch(/application\/json/); + }); + + it("returns 500 JSON response for errors on /v1/* API paths", async () => { + const response = await request(app) + .get("/v1/test-error") + .expect(500); + + expect(response.body).toEqual({ + error: "Internal server error" + }); + expect(response.headers["content-type"]).toMatch(/application\/json/); + }); + + it("returns 500 JSON response for errors on /health API paths", async () => { + const response = await request(app) + .get("/health/test-error") + .expect(500); + + expect(response.body).toEqual({ + error: "Internal server error" + }); + expect(response.headers["content-type"]).toMatch(/application\/json/); + }); + + it("returns 500 plain text response for errors on non-API paths", async () => { + const response = await request(app) + .get("/non-api/test-error") + .expect(500); + + expect(response.text).toBe("Internal server error"); + expect(response.headers["content-type"]).toMatch(/text\/html/); + }); + + it("handles POST requests with valid JSON but route throws error (API path)", async () => { + const response = await request(app) + .post("/v1/convert/html") + .set("Content-Type", "application/json") + .send({ html: "

Test

" }) + .expect(500); + + expect(response.body).toEqual({ + error: "Internal server error" + }); + expect(response.headers["content-type"]).toMatch(/application\/json/); + }); +}); \ No newline at end of file