import { describe, it, expect, vi, beforeEach } from "vitest"; const pipelineChain = vi.hoisted(() => ({ zadd: vi.fn().mockReturnThis(), zremrangebyscore: vi.fn().mockReturnThis(), zcard: vi.fn().mockReturnThis(), expire: vi.fn().mockReturnThis(), exec: vi.fn(), })); vi.mock("../redis", () => ({ getRedis: vi.fn(() => ({ pipeline: vi.fn(() => pipelineChain) })), })); vi.mock("next/server", () => ({ NextResponse: { json: vi.fn((body: unknown, init?: { status?: number }) => ({ body, init, isNextResponse: true })), }, })); import { NextResponse } from "next/server"; const { applyRateLimit } = await import("../rate-limit"); beforeEach(() => { vi.clearAllMocks(); }); describe("applyRateLimit", () => { it("returns null when under the limit", async () => { pipelineChain.exec.mockResolvedValue([null, null, [null, 3], null]); const result = await applyRateLimit("test-key", 10, 60); expect(result).toBeNull(); }); it("returns 429 response when over the limit", async () => { pipelineChain.exec.mockResolvedValue([null, null, [null, 11], null]); const result = await applyRateLimit("test-key", 10, 60); expect(result).not.toBeNull(); expect(NextResponse.json).toHaveBeenCalledWith( expect.objectContaining({ error: expect.any(String) }), expect.objectContaining({ status: 429 }) ); }); it("returns null when exactly at limit (count === limit is allowed)", async () => { pipelineChain.exec.mockResolvedValue([null, null, [null, 10], null]); const result = await applyRateLimit("test-key", 10, 60); expect(result).toBeNull(); }); it("calls pipeline with zadd, zremrangebyscore, zcard, expire", async () => { pipelineChain.exec.mockResolvedValue([null, null, [null, 1], null]); await applyRateLimit("my-key", 5, 30); expect(pipelineChain.zadd).toHaveBeenCalled(); expect(pipelineChain.zremrangebyscore).toHaveBeenCalled(); expect(pipelineChain.zcard).toHaveBeenCalled(); expect(pipelineChain.expire).toHaveBeenCalled(); }); });