test: improve db.ts and keys.ts coverage
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
This commit is contained in:
parent
db35a0e521
commit
ae8b32e1c4
2 changed files with 228 additions and 0 deletions
99
src/__tests__/db-init-cleanup.test.ts
Normal file
99
src/__tests__/db-init-cleanup.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock pg and logger like db.test.ts does
|
||||
const mockRelease = vi.fn();
|
||||
const mockQuery = vi.fn();
|
||||
const mockConnect = vi.fn();
|
||||
|
||||
vi.mock("pg", () => {
|
||||
const Pool = vi.fn(function () {
|
||||
return {
|
||||
connect: mockConnect,
|
||||
on: vi.fn(),
|
||||
};
|
||||
});
|
||||
return { default: { Pool }, Pool };
|
||||
});
|
||||
|
||||
vi.mock("../services/logger.js", () => ({
|
||||
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
// Use real db.js implementation
|
||||
vi.mock("../services/db.js", async () => {
|
||||
return await vi.importActual("../services/db.js");
|
||||
});
|
||||
|
||||
describe("initDatabase", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConnect.mockReset();
|
||||
mockQuery.mockReset();
|
||||
mockRelease.mockReset();
|
||||
});
|
||||
|
||||
it("calls connectWithRetry, runs DDL, and releases client", async () => {
|
||||
// connectWithRetry does pool.connect() then SELECT 1 validation
|
||||
mockQuery.mockResolvedValue({ rows: [], rowCount: 0 });
|
||||
mockConnect.mockResolvedValue({ query: mockQuery, release: mockRelease });
|
||||
|
||||
const { initDatabase } = await import("../services/db.js");
|
||||
await initDatabase();
|
||||
|
||||
// SELECT 1 validation + DDL = at least 2 calls
|
||||
expect(mockQuery).toHaveBeenCalledTimes(2);
|
||||
// DDL should contain CREATE TABLE
|
||||
const ddlCall = mockQuery.mock.calls[1][0] as string;
|
||||
expect(ddlCall).toContain("CREATE TABLE IF NOT EXISTS api_keys");
|
||||
expect(ddlCall).toContain("CREATE TABLE IF NOT EXISTS usage");
|
||||
expect(mockRelease).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("releases client even if DDL fails", async () => {
|
||||
const selectQuery = vi.fn()
|
||||
.mockResolvedValueOnce({ rows: [{ "?column?": 1 }] }) // SELECT 1
|
||||
.mockRejectedValueOnce(new Error("DDL failed")); // DDL
|
||||
mockConnect.mockResolvedValue({ query: selectQuery, release: mockRelease });
|
||||
|
||||
const { initDatabase } = await import("../services/db.js");
|
||||
await expect(initDatabase()).rejects.toThrow("DDL failed");
|
||||
expect(mockRelease).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanupStaleData", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockConnect.mockReset();
|
||||
mockQuery.mockReset();
|
||||
mockRelease.mockReset();
|
||||
});
|
||||
|
||||
it("deletes expired verifications and orphaned usage, returns counts", async () => {
|
||||
// queryWithRetry: connect → query → release for each call
|
||||
const client = { query: mockQuery, release: mockRelease };
|
||||
mockConnect.mockResolvedValue(client);
|
||||
|
||||
// First queryWithRetry call: expired verifications
|
||||
mockQuery.mockResolvedValueOnce({ rows: [{ email: "a@b.com" }, { email: "c@d.com" }], rowCount: 2 });
|
||||
// Second queryWithRetry call: orphaned usage
|
||||
mockQuery.mockResolvedValueOnce({ rows: [{ key: "old_key" }], rowCount: 1 });
|
||||
|
||||
const { cleanupStaleData } = await import("../services/db.js");
|
||||
const result = await cleanupStaleData();
|
||||
|
||||
expect(result).toEqual({ expiredVerifications: 2, orphanedUsage: 1 });
|
||||
});
|
||||
|
||||
it("returns zeros when nothing to clean", async () => {
|
||||
const client = { query: mockQuery, release: mockRelease };
|
||||
mockConnect.mockResolvedValue(client);
|
||||
|
||||
mockQuery.mockResolvedValue({ rows: [], rowCount: 0 });
|
||||
|
||||
const { cleanupStaleData } = await import("../services/db.js");
|
||||
const result = await cleanupStaleData();
|
||||
|
||||
expect(result).toEqual({ expiredVerifications: 0, orphanedUsage: 0 });
|
||||
});
|
||||
});
|
||||
129
src/__tests__/keys-coverage.test.ts
Normal file
129
src/__tests__/keys-coverage.test.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Unmock keys service — we want real implementation
|
||||
vi.unmock("../services/keys.js");
|
||||
|
||||
// DB is still mocked by setup.ts
|
||||
import { queryWithRetry } from "../services/db.js";
|
||||
|
||||
describe("keys service — uncovered branches", () => {
|
||||
let keys: typeof import("../services/keys.js");
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
keys = await import("../services/keys.js");
|
||||
});
|
||||
|
||||
describe("loadKeys error path", () => {
|
||||
it("sets keysCache to empty array when query fails (ignoring env seeds)", async () => {
|
||||
const origKeys = process.env.API_KEYS;
|
||||
delete process.env.API_KEYS;
|
||||
|
||||
vi.mocked(queryWithRetry).mockRejectedValueOnce(new Error("DB down"));
|
||||
await keys.loadKeys();
|
||||
|
||||
// Cache should be empty — no keys valid
|
||||
expect(keys.isValidKey("anything")).toBe(false);
|
||||
expect(keys.getAllKeys()).toEqual([]);
|
||||
|
||||
process.env.API_KEYS = origKeys;
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadKeys with API_KEYS env var", () => {
|
||||
const origEnv = process.env.API_KEYS;
|
||||
|
||||
afterEach(() => {
|
||||
if (origEnv !== undefined) {
|
||||
process.env.API_KEYS = origEnv;
|
||||
} else {
|
||||
delete process.env.API_KEYS;
|
||||
}
|
||||
});
|
||||
|
||||
it("seeds keys from API_KEYS env var into cache and upserts to DB", async () => {
|
||||
process.env.API_KEYS = "seed_key_1,seed_key_2";
|
||||
|
||||
// loadKeys query returns empty (no existing keys)
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
// Two upsert calls for seed keys
|
||||
vi.mocked(queryWithRetry).mockResolvedValue({ rows: [], rowCount: 0 } as any);
|
||||
|
||||
await keys.loadKeys();
|
||||
|
||||
expect(keys.isValidKey("seed_key_1")).toBe(true);
|
||||
expect(keys.isValidKey("seed_key_2")).toBe(true);
|
||||
expect(keys.getKeyInfo("seed_key_1")?.tier).toBe("pro");
|
||||
expect(keys.getKeyInfo("seed_key_1")?.email).toBe("seed@docfast.dev");
|
||||
|
||||
// 1 SELECT + 2 upserts
|
||||
expect(vi.mocked(queryWithRetry)).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("does not duplicate seed keys already in cache", async () => {
|
||||
process.env.API_KEYS = "existing_key";
|
||||
|
||||
// loadKeys returns the key already in DB
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{ key: "existing_key", tier: "pro", email: "x@y.com", created_at: "2025-01-01T00:00:00Z", stripe_customer_id: null }],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
|
||||
await keys.loadKeys();
|
||||
|
||||
// Should not upsert — key already exists
|
||||
expect(vi.mocked(queryWithRetry)).toHaveBeenCalledTimes(1);
|
||||
expect(keys.getAllKeys()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFreeKey — email already exists in cache", () => {
|
||||
it("returns existing free key for same email without DB insert", async () => {
|
||||
// Load empty cache
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({ rows: [], rowCount: 0 } as any);
|
||||
await keys.loadKeys();
|
||||
|
||||
// First create
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({ rows: [], rowCount: 1 } as any);
|
||||
const first = await keys.createFreeKey("dup@test.com");
|
||||
|
||||
// Second call should return cached key without hitting DB
|
||||
vi.mocked(queryWithRetry).mockClear();
|
||||
const second = await keys.createFreeKey("dup@test.com");
|
||||
|
||||
expect(second.key).toBe(first.key);
|
||||
expect(vi.mocked(queryWithRetry)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createProKey — customer already in cache", () => {
|
||||
it("updates tier to pro and returns existing entry", async () => {
|
||||
// Load cache with a free key that has a stripe customer
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({
|
||||
rows: [{
|
||||
key: "df_free_old",
|
||||
tier: "free",
|
||||
email: "user@test.com",
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
stripe_customer_id: "cus_existing",
|
||||
}],
|
||||
rowCount: 1,
|
||||
} as any);
|
||||
await keys.loadKeys();
|
||||
|
||||
// Clear mock call history from loadKeys
|
||||
vi.mocked(queryWithRetry).mockClear();
|
||||
// The UPDATE query
|
||||
vi.mocked(queryWithRetry).mockResolvedValueOnce({ rows: [], rowCount: 1 } as any);
|
||||
|
||||
const result = await keys.createProKey("user@test.com", "cus_existing");
|
||||
|
||||
expect(result.key).toBe("df_free_old");
|
||||
expect(result.tier).toBe("pro");
|
||||
// Should have called UPDATE, not INSERT
|
||||
const updateCall = vi.mocked(queryWithRetry).mock.calls[0];
|
||||
expect(updateCall[0]).toContain("UPDATE");
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue