diff --git a/src/__tests__/email-change-branch-coverage.test.ts b/src/__tests__/email-change-branch-coverage.test.ts new file mode 100644 index 0000000..e8ac5f6 --- /dev/null +++ b/src/__tests__/email-change-branch-coverage.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import express from "express"; +import request from "supertest"; + +vi.mock("../services/db.js"); +vi.mock("../services/logger.js", () => ({ + default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, +})); + +let app: express.Express; + +beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + + const { queryWithRetry } = await import("../services/db.js"); + vi.mocked(queryWithRetry).mockResolvedValue({ rows: [], rowCount: 0 } as any); + + const { emailChangeRouter } = await import("../routes/email-change.js"); + app = express(); + app.use(express.json()); + app.use("/email-change", emailChangeRouter); +}); + +describe("email-change branch coverage", () => { + // Line 16: req.body being falsy in rate limiter keyGenerator — covered implicitly + // Line 75: req.body being falsy in POST / handler + it("POST /email-change with no body returns 400", async () => { + const res = await request(app) + .post("/email-change") + .set("Content-Type", "application/json") + .send("null"); + expect(res.status).toBe(400); + }); + + // Line 171: req.body being falsy in POST /verify handler + it("POST /email-change/verify with no body returns 400", async () => { + const res = await request(app) + .post("/email-change/verify") + .set("Content-Type", "application/json") + .send("null"); + expect(res.status).toBe(400); + }); +}); diff --git a/src/__tests__/recover-branch-coverage.test.ts b/src/__tests__/recover-branch-coverage.test.ts new file mode 100644 index 0000000..bf6c4fc --- /dev/null +++ b/src/__tests__/recover-branch-coverage.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import express from "express"; +import request from "supertest"; + +vi.mock("../services/db.js"); +vi.mock("../services/logger.js", () => ({ + default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, +})); + +let app: express.Express; + +beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + + const { createPendingVerification, verifyCode } = await import("../services/verification.js"); + const { sendVerificationEmail } = await import("../services/email.js"); + const { getAllKeys } = await import("../services/keys.js"); + const { queryWithRetry } = await import("../services/db.js"); + + vi.mocked(createPendingVerification).mockResolvedValue({ email: "test@test.com", code: "654321", createdAt: "", expiresAt: "", attempts: 0 }); + vi.mocked(verifyCode).mockResolvedValue({ status: "ok" }); + vi.mocked(sendVerificationEmail).mockResolvedValue(true); + vi.mocked(getAllKeys).mockReturnValue([]); + vi.mocked(queryWithRetry).mockResolvedValue({ rows: [], rowCount: 0 } as any); + + const { recoverRouter } = await import("../routes/recover.js"); + app = express(); + app.use(express.json()); + app.use("/recover", recoverRouter); +}); + +describe("recover branch coverage", () => { + // Line 61: req.body being falsy (no body sent) — hits `req.body || {}` fallback + it("POST /recover with no body returns 400", async () => { + const res = await request(app) + .post("/recover") + .set("Content-Type", "application/json") + .send("null"); + expect(res.status).toBe(400); + }); + + // Line 151: req.body being falsy in verify endpoint + it("POST /recover/verify with no body returns 400", async () => { + const res = await request(app) + .post("/recover/verify") + .set("Content-Type", "application/json") + .send("null"); + expect(res.status).toBe(400); + }); + + // Line 181: row.created_at instanceof Date — when DB returns a Date object + it("POST /recover/verify handles Date object for created_at in DB fallback", async () => { + const { verifyCode } = await import("../services/verification.js"); + const { getAllKeys } = await import("../services/keys.js"); + const { queryWithRetry } = await import("../services/db.js"); + + vi.mocked(verifyCode).mockResolvedValue({ status: "ok" }); + vi.mocked(getAllKeys).mockReturnValue([]); // Force DB fallback + vi.mocked(queryWithRetry).mockResolvedValue({ + rows: [{ + key: "recovered-key", + tier: "pro", + email: "found@test.com", + created_at: new Date("2026-01-15T10:00:00Z"), // Date object, not string + stripe_customer_id: null, + }], + rowCount: 1, + } as any); + + const res = await request(app) + .post("/recover/verify") + .send({ email: "found@test.com", code: "654321" }); + + expect(res.status).toBe(200); + expect(res.body.apiKey).toBe("recovered-key"); + }); +});