diff --git a/src/__tests__/pdf-rate-limit-coverage.test.ts b/src/__tests__/pdf-rate-limit-coverage.test.ts new file mode 100644 index 0000000..a1a4943 --- /dev/null +++ b/src/__tests__/pdf-rate-limit-coverage.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +// We need to reset module state between tests since pdfRateLimit uses module-level variables +async function freshImport() { + // Clear module cache to reset activePdfCount, pdfQueue, etc. + const modPath = "../middleware/pdfRateLimit.js"; + // vitest handles this via vi.resetModules + const mod = await import("../middleware/pdfRateLimit.js"); + return mod; +} + +// Mock dependencies +vi.mock("../services/keys.js", () => ({ + isProKey: (key: string) => key.startsWith("pro_"), +})); + +vi.mock("../services/logger.js", () => ({ + default: { warn: vi.fn(), info: vi.fn(), error: vi.fn() }, +})); + +describe("pdfRateLimit coverage gaps", () => { + beforeEach(() => { + vi.resetModules(); + vi.useFakeTimers(); + }); + + it("getConcurrencyStats returns current state", async () => { + const { getConcurrencyStats } = await import("../middleware/pdfRateLimit.js"); + const stats = getConcurrencyStats(); + expect(stats).toEqual({ + activePdfCount: 0, + queueSize: 0, + maxConcurrent: 3, + maxQueue: 10, + }); + }); + + it("releaseConcurrencySlot resolves a queued waiter", async () => { + const { pdfRateLimitMiddleware, getConcurrencyStats } = await import("../middleware/pdfRateLimit.js"); + + // Create mock req/res/next to get acquirePdfSlot/releasePdfSlot + const slots: { acquire: () => Promise; release: () => void }[] = []; + + function makeReqRes() { + const req: any = { apiKeyInfo: { key: "test_key_1" } }; + const res: any = { + set: vi.fn(), + status: vi.fn().mockReturnThis(), + json: vi.fn(), + }; + const next = vi.fn(); + pdfRateLimitMiddleware(req, res, next); + return { acquire: req.acquirePdfSlot, release: req.releasePdfSlot }; + } + + // Fill all 3 concurrency slots + const s1 = makeReqRes(); + await s1.acquire(); + const s2 = makeReqRes(); + await s2.acquire(); + const s3 = makeReqRes(); + await s3.acquire(); + + expect(getConcurrencyStats().activePdfCount).toBe(3); + + // 4th request should queue + const s4 = makeReqRes(); + let s4Resolved = false; + const s4Promise = s4.acquire().then(() => { s4Resolved = true; }); + + expect(getConcurrencyStats().queueSize).toBe(1); + expect(s4Resolved).toBe(false); + + // Release one slot — should resolve the waiter (lines 116-117) + s1.release(); + + // Let microtasks flush + await vi.advanceTimersByTimeAsync(0); + await s4Promise; + + expect(s4Resolved).toBe(true); + // Active count should still be 3 (waiter took the slot) + expect(getConcurrencyStats().activePdfCount).toBe(3); + expect(getConcurrencyStats().queueSize).toBe(0); + }); + + it("throws QUEUE_FULL when per-key queue limit reached (lines 101-103)", async () => { + const { pdfRateLimitMiddleware } = await import("../middleware/pdfRateLimit.js"); + + function makeReqRes(key: string) { + const req: any = { apiKeyInfo: { key } }; + const res: any = { + set: vi.fn(), + status: vi.fn().mockReturnThis(), + json: vi.fn(), + }; + const next = vi.fn(); + pdfRateLimitMiddleware(req, res, next); + return { acquire: req.acquirePdfSlot, release: req.releasePdfSlot }; + } + + // Fill concurrency slots with different keys + await makeReqRes("other_1").acquire(); + await makeReqRes("other_2").acquire(); + await makeReqRes("other_3").acquire(); + + // Queue 3 requests for same key (MAX_QUEUED_PER_KEY = 3) + const sameKey = "same_key"; + makeReqRes(sameKey).acquire(); // queued #1 + makeReqRes(sameKey).acquire(); // queued #2 + makeReqRes(sameKey).acquire(); // queued #3 + + // 4th from same key should throw QUEUE_FULL + await expect(makeReqRes(sameKey).acquire()).rejects.toThrow("QUEUE_FULL"); + }); +});