diff --git a/src/__tests__/keys-cache-hit.test.ts b/src/__tests__/keys-cache-hit.test.ts new file mode 100644 index 0000000..ec8be7e --- /dev/null +++ b/src/__tests__/keys-cache-hit.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.unmock("../services/keys.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() }, +})); + +import { queryWithRetry } from "../services/db.js"; +import { loadKeys, createProKey, downgradeByCustomer, findKeyByCustomerId, getAllKeys } from "../services/keys.js"; + +const mockQuery = vi.mocked(queryWithRetry); + +describe("keys cache-hit paths", () => { + beforeEach(async () => { + vi.clearAllMocks(); + // Reset cache via loadKeys with empty result + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); + await loadKeys(); + vi.clearAllMocks(); + }); + + describe("createProKey - cache UPSERT update (line 142)", () => { + it("updates existing cache entry on second call with same stripeCustomerId", async () => { + // First call: creates entry and pushes to cache (else branch) + const firstResult = { + key: "df_pro_first", + tier: "pro", + email: "first@test.com", + created_at: "2026-01-01T00:00:00.000Z", + stripe_customer_id: "cus_repeat", + }; + mockQuery.mockResolvedValueOnce({ rows: [firstResult], rowCount: 1 } as any); + await createProKey("first@test.com", "cus_repeat"); + + // Second call: same stripeCustomerId → cacheIdx >= 0 → updates in place (line 142) + const secondResult = { + key: "df_pro_first", + tier: "pro", + email: "first@test.com", + created_at: "2026-01-01T00:00:00.000Z", + stripe_customer_id: "cus_repeat", + }; + mockQuery.mockResolvedValueOnce({ rows: [secondResult], rowCount: 1 } as any); + const result = await createProKey("second@test.com", "cus_repeat"); + + expect(result.key).toBe("df_pro_first"); + + // Cache should have exactly 1 entry for this customer (updated, not duplicated) + const allKeys = getAllKeys(); + const matching = allKeys.filter((k) => k.stripeCustomerId === "cus_repeat"); + expect(matching).toHaveLength(1); + }); + }); + + describe("downgradeByCustomer - cache HIT (lines 153-155)", () => { + it("downgrades cached entry to free tier", async () => { + // First, populate cache via createProKey + const entry = { + key: "df_pro_downgrade", + tier: "pro", + email: "downgrade@test.com", + created_at: "2026-01-01T00:00:00.000Z", + stripe_customer_id: "cus_downgrade", + }; + mockQuery.mockResolvedValueOnce({ rows: [entry], rowCount: 1 } as any); + await createProKey("downgrade@test.com", "cus_downgrade"); + vi.clearAllMocks(); + + // Now downgrade — entry is in cache + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // UPDATE + const result = await downgradeByCustomer("cus_downgrade"); + + expect(result).toBe(true); + expect(mockQuery).toHaveBeenCalledWith( + expect.stringContaining("UPDATE"), + expect.arrayContaining(["cus_downgrade"]) + ); + + const allKeys = getAllKeys(); + const found = allKeys.find((k) => k.stripeCustomerId === "cus_downgrade"); + expect(found?.tier).toBe("free"); + }); + }); + + describe("findKeyByCustomerId (line 175)", () => { + it("finds key by stripe customer ID via DB lookup", async () => { + mockQuery.mockResolvedValueOnce({ + rows: [{ + key: "df_pro_found", + tier: "pro", + email: "found@test.com", + created_at: "2026-01-01T00:00:00.000Z", + stripe_customer_id: "cus_find_me", + }], + rowCount: 1, + } as any); + + const result = await findKeyByCustomerId("cus_find_me"); + expect(result).not.toBeNull(); + expect(result!.key).toBe("df_pro_found"); + }); + + it("returns null for unknown customer ID", async () => { + mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any); + const result = await findKeyByCustomerId("cus_unknown"); + expect(result).toBeNull(); + }); + }); +});