import { describe, it, expect, vi, beforeEach } from "vitest"; // Unmock keys to use real getAllKeys (but we'll mock it ourselves) vi.unmock("../services/keys.js"); vi.unmock("../services/verification.js"); vi.unmock("../services/email.js"); 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/logger.js", () => ({ default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }, })); vi.mock("../services/verification.js", () => ({ createPendingVerification: vi.fn(), verifyCode: vi.fn(), })); vi.mock("../services/email.js", () => ({ sendVerificationEmail: vi.fn().mockResolvedValue(undefined), })); vi.mock("../services/keys.js", () => ({ getAllKeys: vi.fn().mockReturnValue([]), loadKeys: vi.fn().mockResolvedValue(undefined), isValidKey: vi.fn(), getKeyInfo: vi.fn(), isProKey: vi.fn(), createFreeKey: vi.fn(), createProKey: vi.fn(), downgradeByCustomer: vi.fn(), findKeyByCustomerId: vi.fn(), updateKeyEmail: vi.fn(), updateEmailByCustomer: vi.fn(), })); import { queryWithRetry } from "../services/db.js"; import { getAllKeys } from "../services/keys.js"; import { verifyCode } from "../services/verification.js"; import express from "express"; import request from "supertest"; import { recoverRouter } from "../routes/recover.js"; const mockQuery = vi.mocked(queryWithRetry); const mockGetAllKeys = vi.mocked(getAllKeys); const mockVerifyCode = vi.mocked(verifyCode); function createApp() { const app = express(); app.use(express.json()); app.use("/v1/recover", recoverRouter); return app; } describe("recover verify DB fallback", () => { beforeEach(() => { vi.clearAllMocks(); }); it("falls back to DB when cache has no matching email after verify succeeds", async () => { mockVerifyCode.mockResolvedValueOnce({ status: "ok" } as any); mockGetAllKeys.mockReturnValueOnce([]); // cache empty // DB fallback returns the key mockQuery.mockResolvedValueOnce({ rows: [ { key: "df_pro_xyz", tier: "pro", email: "user@test.com", created_at: "2026-01-01T00:00:00.000Z", stripe_customer_id: null, }, ], rowCount: 1, } as any); const app = createApp(); const res = await request(app) .post("/v1/recover/verify") .send({ email: "user@test.com", code: "123456" }); expect(res.status).toBe(200); expect(res.body.status).toBe("recovered"); expect(res.body.apiKey).toBe("df_pro_xyz"); expect(res.body.tier).toBe("pro"); expect(mockQuery).toHaveBeenCalledWith( expect.stringContaining("SELECT"), expect.arrayContaining(["user@test.com"]) ); }); it("returns 'no key found' when not in cache AND not in DB", async () => { mockVerifyCode.mockResolvedValueOnce({ status: "ok" } as any); mockGetAllKeys.mockReturnValueOnce([]); mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); const app = createApp(); const res = await request(app) .post("/v1/recover/verify") .send({ email: "nobody@test.com", code: "123456" }); expect(res.status).toBe(200); expect(res.body.status).toBe("recovered"); expect(res.body.apiKey).toBeUndefined(); expect(res.body.message).toContain("No API key found"); }); });