diff --git a/src/__tests__/pdfRateLimit-coverage.test.ts b/src/__tests__/pdfRateLimit-coverage.test.ts new file mode 100644 index 0000000..b074ac9 --- /dev/null +++ b/src/__tests__/pdfRateLimit-coverage.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { isProKey } from "../services/keys.js"; + +// Tests to improve coverage for pdfRateLimit.ts +// Target: per-key queue fairness rejection and cleanupExpiredEntries behavior + +const mockNext = vi.fn(); +const headers: Record = {}; +const mockSet = vi.fn((k: string, v: string) => { headers[k] = v; }); +const mockJson = vi.fn(); +const mockStatus = vi.fn(() => ({ json: mockJson })); + +function makeReq(key = "test-key"): any { + return { + apiKeyInfo: { key, tier: "free", email: "t@t.com", createdAt: "2025-01-01" }, + headers: {}, + }; +} + +function makeRes(): any { + Object.keys(headers).forEach((k) => delete headers[k]); + mockSet.mockClear(); + mockJson.mockClear(); + mockStatus.mockClear(); + return { set: mockSet, status: mockStatus, json: mockJson }; +} + +describe("pdfRateLimit middleware - additional coverage", () => { + let pdfRateLimitMiddleware: any; + + beforeEach(async () => { + vi.clearAllMocks(); + vi.resetModules(); + const mod = await import("../middleware/pdfRateLimit.js"); + pdfRateLimitMiddleware = mod.pdfRateLimitMiddleware; + }); + + it("should reject per-key queue fairness when MAX_QUEUED_PER_KEY (3) exceeded", async () => { + vi.mocked(isProKey).mockReturnValue(false); + + // Fill up 3 concurrent slots with different keys + const concurrentReqs = []; + for (let i = 0; i < 3; i++) { + const req = makeReq(`concurrent-${i}`); + const res = makeRes(); + pdfRateLimitMiddleware(req, res, mockNext); + concurrentReqs.push(req); + await (req as any).acquirePdfSlot(); // Acquire but don't release + } + + // Now try to queue 4 requests with the same key (should only allow 3 per key) + const sameKeyPromises = []; + const sameKey = "same-key"; + + // Queue 3 requests with the same key (this should work) + for (let i = 0; i < 3; i++) { + const req = makeReq(sameKey); + const res = makeRes(); + pdfRateLimitMiddleware(req, res, mockNext); + sameKeyPromises.push((req as any).acquirePdfSlot()); + } + + // Try to queue a 4th request with the same key - this should fail with QUEUE_FULL + const req4th = makeReq(sameKey); + const res4th = makeRes(); + pdfRateLimitMiddleware(req4th, res4th, mockNext); + + await expect((req4th as any).acquirePdfSlot()).rejects.toThrow("QUEUE_FULL"); + }); + + it("should call cleanupExpiredEntries and remove expired rate limit entries", async () => { + vi.mocked(isProKey).mockReturnValue(false); + vi.useFakeTimers(); + + try { + // Create a rate limit entry + const req = makeReq("cleanup-test-key"); + const res = makeRes(); + pdfRateLimitMiddleware(req, res, mockNext); + + // Verify it was allowed (first request) + expect(mockNext).toHaveBeenCalled(); + mockNext.mockClear(); + + // Advance time past the rate limit window (60s + 1ms) + vi.advanceTimersByTime(60_001); + + // Make another request - this should trigger cleanup and reset the rate limit + const req2 = makeReq("cleanup-test-key"); + const res2 = makeRes(); + pdfRateLimitMiddleware(req2, res2, mockNext); + + // Should be allowed again since cleanup removed the expired entry + expect(mockNext).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith("X-RateLimit-Remaining", "9"); // Should be 9 (10-1) + + } finally { + vi.useRealTimers(); + } + }); + + it("should test automatic cleanup interval calls cleanupExpiredEntries", async () => { + vi.useFakeTimers(); + + try { + // Import the module to trigger the setInterval + const mod = await import("../middleware/pdfRateLimit.js"); + pdfRateLimitMiddleware = mod.pdfRateLimitMiddleware; + + vi.mocked(isProKey).mockReturnValue(false); + + // Create some rate limit entries that will expire + const req1 = makeReq("auto-cleanup-1"); + const res1 = makeRes(); + pdfRateLimitMiddleware(req1, res1, mockNext); + + const req2 = makeReq("auto-cleanup-2"); + const res2 = makeRes(); + pdfRateLimitMiddleware(req2, res2, mockNext); + + // Advance past expiration + vi.advanceTimersByTime(70_000); // 70 seconds + + // Trigger the automatic cleanup by advancing the interval timer + vi.advanceTimersByTime(60_000); // Cleanup runs every 60s + + // Create a new request - should start fresh since old entries were cleaned up + const req3 = makeReq("auto-cleanup-1"); + const res3 = makeRes(); + pdfRateLimitMiddleware(req3, res3, mockNext); + + expect(mockNext).toHaveBeenCalled(); + + } finally { + vi.useRealTimers(); + } + }); + + it("should handle unknown api key in middleware", () => { + vi.mocked(isProKey).mockReturnValue(false); + + // Request without apiKeyInfo (should default to "unknown") + const req = { + apiKeyInfo: undefined, + headers: {}, + }; + const res = makeRes(); + + pdfRateLimitMiddleware(req, res, mockNext); + + // Should still set headers and call next (using "unknown" as key) + expect(mockSet).toHaveBeenCalledWith("X-RateLimit-Limit", "10"); + expect(mockNext).toHaveBeenCalled(); + }); +}); \ No newline at end of file