chore: update @types/pg, add keys.ts branch coverage tests (13 total)
This commit is contained in:
parent
50b4ee3fd4
commit
0a4fcd2e50
3 changed files with 149 additions and 43 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -27,7 +27,7 @@
|
|||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
|
|
@ -1220,9 +1220,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz",
|
||||
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,16 @@ vi.mock("../services/logger.js", () => ({
|
|||
}));
|
||||
|
||||
import { queryWithRetry } from "../services/db.js";
|
||||
import { createProKey, downgradeByCustomer, updateKeyEmail, updateEmailByCustomer } from "../services/keys.js";
|
||||
import {
|
||||
loadKeys,
|
||||
getAllKeys,
|
||||
createFreeKey,
|
||||
createProKey,
|
||||
findKeyInCacheOrDb,
|
||||
downgradeByCustomer,
|
||||
updateKeyEmail,
|
||||
updateEmailByCustomer,
|
||||
} from "../services/keys.js";
|
||||
|
||||
const mockQuery = vi.mocked(queryWithRetry);
|
||||
|
||||
|
|
@ -27,11 +36,138 @@ describe("Keys Branch Coverage", () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// === NEW: Date conversion and cache branches ===
|
||||
|
||||
describe("loadKeys — Date branch for created_at (line 46)", () => {
|
||||
it("converts Date objects to ISO strings in loadKeys", async () => {
|
||||
const dateObj = new Date("2026-01-15T10:00:00.000Z");
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
key: "df_free_abc",
|
||||
tier: "free",
|
||||
email: "test@example.com",
|
||||
created_at: dateObj,
|
||||
stripe_customer_id: null,
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
delete process.env.API_KEYS;
|
||||
await loadKeys();
|
||||
|
||||
const keys = getAllKeys();
|
||||
const loaded = keys.find((k) => k.key === "df_free_abc");
|
||||
expect(loaded).toBeDefined();
|
||||
expect(loaded!.createdAt).toBe("2026-01-15T10:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("findKeyInCacheOrDb — Date branch for created_at", () => {
|
||||
it("converts Date objects to ISO strings in findKeyInCacheOrDb", async () => {
|
||||
const dateObj = new Date("2026-02-20T12:00:00.000Z");
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
key: "df_pro_xyz",
|
||||
tier: "pro",
|
||||
email: "pro@example.com",
|
||||
created_at: dateObj,
|
||||
stripe_customer_id: "cus_abc",
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
const result = await findKeyInCacheOrDb("key", "df_pro_xyz");
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.createdAt).toBe("2026-02-20T12:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFreeKey — existing email in cache (line 88)", () => {
|
||||
it("returns existing free key when email already in cache", async () => {
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
key: "df_free_existing",
|
||||
tier: "free",
|
||||
email: "cached@example.com",
|
||||
created_at: "2026-01-01T00:00:00.000Z",
|
||||
stripe_customer_id: null,
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
delete process.env.API_KEYS;
|
||||
await loadKeys();
|
||||
|
||||
const result = await createFreeKey("cached@example.com");
|
||||
expect(result.key).toBe("df_free_existing");
|
||||
expect(result.email).toBe("cached@example.com");
|
||||
expect(mockQuery).toHaveBeenCalledTimes(1); // only the loadKeys call
|
||||
});
|
||||
});
|
||||
|
||||
describe("createProKey — Date branch in RETURNING row (line 135)", () => {
|
||||
it("converts Date created_at in createProKey RETURNING row", async () => {
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
delete process.env.API_KEYS;
|
||||
await loadKeys();
|
||||
|
||||
const dateObj = new Date("2026-03-01T08:00:00.000Z");
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
key: "df_pro_new123",
|
||||
tier: "pro",
|
||||
email: "pro@example.com",
|
||||
created_at: dateObj,
|
||||
stripe_customer_id: "cus_new",
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
const result = await createProKey("pro@example.com", "cus_new");
|
||||
expect(result.createdAt).toBe("2026-03-01T08:00:00.000Z");
|
||||
expect(result.key).toBe("df_pro_new123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createProKey — cache miss path, push to cache (line 141)", () => {
|
||||
it("pushes new entry to cache when stripeCustomerId not in cache", async () => {
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
delete process.env.API_KEYS;
|
||||
await loadKeys();
|
||||
|
||||
mockQuery.mockResolvedValueOnce({
|
||||
rows: [
|
||||
{
|
||||
key: "df_pro_pushed",
|
||||
tier: "pro",
|
||||
email: "new@example.com",
|
||||
created_at: "2026-03-15T10:00:00.000Z",
|
||||
stripe_customer_id: "cus_brand_new",
|
||||
},
|
||||
],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
const result = await createProKey("new@example.com", "cus_brand_new");
|
||||
expect(result.key).toBe("df_pro_pushed");
|
||||
expect(result.stripeCustomerId).toBe("cus_brand_new");
|
||||
|
||||
const keys = getAllKeys();
|
||||
expect(keys.some((k) => k.key === "df_pro_pushed")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// === ORIGINAL: UPSERT conflict, downgrade, updateKeyEmail, updateEmailByCustomer ===
|
||||
|
||||
describe("createProKey - UPSERT conflict path (line 142)", () => {
|
||||
it("should return existing key when stripe_customer_id already exists in DB but NOT in cache", async () => {
|
||||
// Scenario: Another pod created a key for this customer, so it's in DB but not in our cache
|
||||
// The UPSERT will hit ON CONFLICT and return the existing key via RETURNING clause
|
||||
|
||||
const existingKey = {
|
||||
key: "df_pro_existing_abc",
|
||||
tier: "pro",
|
||||
|
|
@ -40,18 +176,15 @@ describe("Keys Branch Coverage", () => {
|
|||
stripe_customer_id: "cus_existing",
|
||||
};
|
||||
|
||||
// Mock: UPSERT returns the existing key (ON CONFLICT triggered)
|
||||
mockQuery.mockResolvedValueOnce({ rows: [existingKey], rowCount: 1 } as any);
|
||||
|
||||
const result = await createProKey("new@test.com", "cus_existing");
|
||||
|
||||
// Should return the existing key
|
||||
expect(result.key).toBe("df_pro_existing_abc");
|
||||
expect(result.email).toBe("existing@test.com"); // Original email, not the new one
|
||||
expect(result.email).toBe("existing@test.com");
|
||||
expect(result.stripeCustomerId).toBe("cus_existing");
|
||||
expect(result.tier).toBe("pro");
|
||||
|
||||
// Verify UPSERT was called
|
||||
const upsertCall = mockQuery.mock.calls.find(
|
||||
(c) => typeof c[0] === "string" && c[0].includes("ON CONFLICT")
|
||||
);
|
||||
|
|
@ -59,12 +192,8 @@ describe("Keys Branch Coverage", () => {
|
|||
});
|
||||
|
||||
it("should handle conflict when inserting new key with existing customer ID", async () => {
|
||||
// First call: load empty cache
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
const { loadKeys } = await import("../services/keys.js");
|
||||
await loadKeys();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
const conflictingKey = {
|
||||
|
|
@ -75,32 +204,27 @@ describe("Keys Branch Coverage", () => {
|
|||
stripe_customer_id: "cus_conflict",
|
||||
};
|
||||
|
||||
// UPSERT returns existing key on conflict
|
||||
mockQuery.mockResolvedValueOnce({ rows: [conflictingKey], rowCount: 1 } as any);
|
||||
|
||||
const result = await createProKey("different-email@test.com", "cus_conflict");
|
||||
|
||||
expect(result.key).toBe("df_pro_conflict_xyz");
|
||||
expect(result.email).toBe("conflict@test.com"); // Original, not the new email
|
||||
expect(result.email).toBe("conflict@test.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("downgradeByCustomer - customer not found (lines 153-155)", () => {
|
||||
it("should return false when customer is NOT in cache AND NOT in DB", async () => {
|
||||
// Mock: SELECT query returns empty (customer not in DB)
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
const result = await downgradeByCustomer("cus_nonexistent");
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Verify SELECT was called
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("SELECT"),
|
||||
expect.arrayContaining(["cus_nonexistent"])
|
||||
);
|
||||
|
||||
// Verify UPDATE was NOT called (no point updating a non-existent key)
|
||||
const updateCalls = mockQuery.mock.calls.filter((c) =>
|
||||
(c[0] as string).includes("UPDATE")
|
||||
);
|
||||
|
|
@ -108,14 +232,10 @@ describe("Keys Branch Coverage", () => {
|
|||
});
|
||||
|
||||
it("should return false for completely unknown stripe customer ID", async () => {
|
||||
// Load empty cache first
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
const { loadKeys } = await import("../services/keys.js");
|
||||
await loadKeys();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock: DB also doesn't have this customer
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
const result = await downgradeByCustomer("cus_unknown_12345");
|
||||
|
|
@ -126,20 +246,16 @@ describe("Keys Branch Coverage", () => {
|
|||
|
||||
describe("updateKeyEmail - DB fallback path (line 175)", () => {
|
||||
it("should return false when key is NOT in cache AND NOT in DB", async () => {
|
||||
// Mock: SELECT query returns empty (key not in DB)
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
const result = await updateKeyEmail("df_pro_nonexistent", "new@test.com");
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Verify SELECT was called
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("SELECT"),
|
||||
expect.arrayContaining(["df_pro_nonexistent"])
|
||||
);
|
||||
|
||||
// Verify UPDATE was NOT called
|
||||
const updateCalls = mockQuery.mock.calls.filter((c) =>
|
||||
(c[0] as string).includes("UPDATE")
|
||||
);
|
||||
|
|
@ -155,16 +271,13 @@ describe("Keys Branch Coverage", () => {
|
|||
stripe_customer_id: "cus_db_only",
|
||||
};
|
||||
|
||||
// Mock: SELECT returns the key from DB
|
||||
mockQuery
|
||||
.mockResolvedValueOnce({ rows: [dbKey], rowCount: 1 } as any)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // UPDATE success
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any);
|
||||
|
||||
const result = await updateKeyEmail("df_pro_db_only", "updated@test.com");
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify UPDATE was called
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE"),
|
||||
expect.arrayContaining(["updated@test.com", "df_pro_db_only"])
|
||||
|
|
@ -174,20 +287,16 @@ describe("Keys Branch Coverage", () => {
|
|||
|
||||
describe("updateEmailByCustomer - DB fallback path (line 175)", () => {
|
||||
it("should return false when customer is NOT in cache AND NOT in DB", async () => {
|
||||
// Mock: SELECT query returns empty
|
||||
mockQuery.mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
const result = await updateEmailByCustomer("cus_nonexistent", "new@test.com");
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
// Verify SELECT was called
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("SELECT"),
|
||||
expect.arrayContaining(["cus_nonexistent"])
|
||||
);
|
||||
|
||||
// Verify UPDATE was NOT called
|
||||
const updateCalls = mockQuery.mock.calls.filter((c) =>
|
||||
(c[0] as string).includes("UPDATE")
|
||||
);
|
||||
|
|
@ -203,16 +312,13 @@ describe("Keys Branch Coverage", () => {
|
|||
stripe_customer_id: "cus_db_customer",
|
||||
};
|
||||
|
||||
// Mock: SELECT returns the key
|
||||
mockQuery
|
||||
.mockResolvedValueOnce({ rows: [dbKey], rowCount: 1 } as any)
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any); // UPDATE success
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 1 } as any);
|
||||
|
||||
const result = await updateEmailByCustomer("cus_db_customer", "newcustomer@test.com");
|
||||
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify UPDATE was called with correct params
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE"),
|
||||
expect.arrayContaining(["newcustomer@test.com", "cus_db_customer"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue