test: improve coverage for health.ts and email-change.ts
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 20m41s

- 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)
This commit is contained in:
OpenClaw Subagent 2026-03-13 20:08:14 +01:00
parent 99b67f2584
commit 8f70a32f77
2 changed files with 58 additions and 0 deletions

View file

@ -90,6 +90,27 @@ describe("POST /v1/email-change", () => {
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.status).toBe("verification_sent"); 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", () => { describe("POST /v1/email-change/verify", () => {
@ -98,6 +119,24 @@ describe("POST /v1/email-change/verify", () => {
expect(res.status).toBe(400); 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 () => { it("returns 400 for invalid code", async () => {
const { verifyCode } = await import("../services/verification.js"); const { verifyCode } = await import("../services/verification.js");
vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" }); vi.mocked(verifyCode).mockResolvedValue({ status: "invalid" });

View file

@ -65,4 +65,23 @@ describe("GET /health", () => {
expect(res.body.version).toBeDefined(); expect(res.body.version).toBeDefined();
expect(typeof res.body.version).toBe("string"); 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);
});
}); });