test: improve keys.ts branch coverage — cache-hit paths for createProKey, downgradeByCustomer, findKeyByCustomerId
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 2m56s
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 2m56s
This commit is contained in:
parent
f5ec837e20
commit
3aae96fd8a
1 changed files with 120 additions and 0 deletions
120
src/__tests__/keys-cache-hit.test.ts
Normal file
120
src/__tests__/keys-cache-hit.test.ts
Normal file
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue