From 8f70a32f779a4fb7c7c70c30519db9d17f364eeb Mon Sep 17 00:00:00 2001 From: OpenClaw Subagent Date: Fri, 13 Mar 2026 20:08:14 +0100 Subject: [PATCH] test: improve coverage for health.ts and email-change.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test for health.ts: client.query() error path with client.release(true) - Add test for email-change.ts: sendVerificationEmail failure (fire-and-forget) - Add test for email-change.ts verify: invalid API key (403 response) Coverage improvements: - health.ts: 87.09% → 100% lines (covered lines 84-85) - email-change.ts: 94.33% → 100% lines, 80% → 100% functions (covered lines 112, 176-177) - Overall: 92.73% → 93.13% lines (+0.40%) - Total tests: 722 → 725 (+3) --- src/__tests__/email-change.test.ts | 39 ++++++++++++++++++++++++++++++ src/__tests__/health.test.ts | 19 +++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/__tests__/email-change.test.ts b/src/__tests__/email-change.test.ts index e87b23c..755f687 100644 --- a/src/__tests__/email-change.test.ts +++ b/src/__tests__/email-change.test.ts @@ -90,6 +90,27 @@ describe("POST /v1/email-change", () => { expect(res.status).toBe(200); expect(res.body.status).toBe("verification_sent"); }); + + it("does not crash when sendVerificationEmail fails (fire-and-forget)", async () => { + const { sendVerificationEmail } = await import("../services/email.js"); + const logger = (await import("../services/logger.js")).default; + + vi.mocked(sendVerificationEmail).mockRejectedValue(new Error("SMTP connection failed")); + + const res = await request(app).post("/v1/email-change").send({ apiKey: "df_pro_xxx", newEmail: "new@example.com" }); + + expect(res.status).toBe(200); + expect(res.body.status).toBe("verification_sent"); + + // Give the catch handler a moment to execute + await new Promise(resolve => setTimeout(resolve, 10)); + + // Verify error was logged + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ email: "new@example.com" }), + "Failed to send email change verification" + ); + }); }); describe("POST /v1/email-change/verify", () => { @@ -98,6 +119,24 @@ describe("POST /v1/email-change/verify", () => { expect(res.status).toBe(400); }); + it("returns 403 for invalid API key", async () => { + const { queryWithRetry } = await import("../services/db.js"); + vi.mocked(queryWithRetry).mockImplementation((async (sql: string) => { + if (sql.includes("SELECT") && sql.includes("key =")) { + return { rows: [], rowCount: 0 }; + } + return { rows: [], rowCount: 0 }; + }) as any); + + const res = await request(app).post("/v1/email-change/verify").send({ + apiKey: "fake", + newEmail: "new@example.com", + code: "123456" + }); + expect(res.status).toBe(403); + expect(res.body.error).toContain("Invalid API key"); + }); + it("returns 400 for invalid code", async () => { const { verifyCode } = await import("../services/verification.js"); vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" }); diff --git a/src/__tests__/health.test.ts b/src/__tests__/health.test.ts index e488e3a..230016d 100644 --- a/src/__tests__/health.test.ts +++ b/src/__tests__/health.test.ts @@ -65,4 +65,23 @@ describe("GET /health", () => { expect(res.body.version).toBeDefined(); expect(typeof res.body.version).toBe("string"); }); + + it("returns 503 when client.query() throws and releases client with destroy flag", async () => { + const mockRelease = vi.fn(); + const mockClient = { + query: vi.fn().mockRejectedValue(new Error("Query failed")), + release: mockRelease, + }; + vi.mocked(pool.connect).mockResolvedValue(mockClient as any); + + const res = await request(app).get("/health"); + + expect(res.status).toBe(503); + expect(res.body.status).toBe("degraded"); + expect(res.body.database.status).toBe("error"); + expect(res.body.database.message).toContain("Query failed"); + + // Verify client.release(true) was called to destroy the bad connection + expect(mockRelease).toHaveBeenCalledWith(true); + }); });