import { describe, it, expect, vi, beforeEach } from "vitest"; import request from "supertest"; import express from "express"; // Mock dependencies vi.mock("../services/keys.js", () => ({ getAllKeys: vi.fn().mockReturnValue([]), isValidKey: vi.fn(), getKeyInfo: vi.fn(), isProKey: vi.fn(), createFreeKey: vi.fn(), createProKey: vi.fn(), downgradeByCustomer: vi.fn(), findKeyByCustomerId: vi.fn(), loadKeys: vi.fn(), updateKeyEmail: vi.fn(), updateEmailByCustomer: vi.fn(), })); vi.mock("../services/db.js", () => ({ default: { query: vi.fn(), connect: vi.fn(), on: vi.fn(), end: vi.fn() }, pool: { query: vi.fn(), connect: vi.fn(), on: vi.fn(), end: vi.fn() }, queryWithRetry: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }), connectWithRetry: vi.fn().mockResolvedValue(undefined), initDatabase: vi.fn().mockResolvedValue(undefined), cleanupStaleData: vi.fn(), isTransientError: vi.fn(), })); vi.mock("../services/verification.js", () => ({ createPendingVerification: vi.fn().mockResolvedValue({ code: "123456" }), verifyCode: vi.fn(), })); vi.mock("../services/email.js", () => ({ sendVerificationEmail: vi.fn().mockResolvedValue(undefined), })); vi.mock("../services/logger.js", () => ({ default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, })); import { getAllKeys } from "../services/keys.js"; import { queryWithRetry } from "../services/db.js"; import { createPendingVerification } from "../services/verification.js"; import { sendVerificationEmail } from "../services/email.js"; import { recoverRouter } from "../routes/recover.js"; const mockGetAllKeys = vi.mocked(getAllKeys); const mockQuery = vi.mocked(queryWithRetry); const mockCreatePending = vi.mocked(createPendingVerification); const mockSendEmail = vi.mocked(sendVerificationEmail); function createApp() { const app = express(); app.use(express.json()); app.use("/v1/recover", recoverRouter); return app; } describe("POST /v1/recover DB fallback (BUG-110)", () => { beforeEach(() => { vi.clearAllMocks(); mockGetAllKeys.mockReturnValue([]); mockCreatePending.mockResolvedValue({ code: "123456" } as any); mockSendEmail.mockResolvedValue(true); }); it("sends verification email via DB fallback when key not in cache but exists in DB", async () => { mockGetAllKeys.mockReturnValue([]); // empty cache mockQuery.mockResolvedValueOnce({ rows: [{ key: "df_pro_abc123" }], rowCount: 1, } as any); const app = createApp(); const res = await request(app) .post("/v1/recover") .send({ email: "user@example.com" }); expect(res.status).toBe(200); expect(res.body.status).toBe("recovery_sent"); expect(mockQuery).toHaveBeenCalledWith( expect.stringContaining("SELECT"), expect.arrayContaining(["user@example.com"]) ); expect(mockCreatePending).toHaveBeenCalledWith("user@example.com"); expect(mockSendEmail).toHaveBeenCalled(); }); it("does NOT send verification email when key not in cache AND not in DB", async () => { mockGetAllKeys.mockReturnValue([]); mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); const app = createApp(); const res = await request(app) .post("/v1/recover") .send({ email: "nobody@example.com" }); expect(res.status).toBe(200); expect(res.body.status).toBe("recovery_sent"); expect(mockCreatePending).not.toHaveBeenCalled(); expect(mockSendEmail).not.toHaveBeenCalled(); }); });