Skip to content

Commit 5b5efe9

Browse files
author
tmdeveloper007
committed
test: add unit tests for rate-limit utility functions
1 parent 6fb6740 commit 5b5efe9

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

test/rate-limit.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { getClientIp, createMemoryFixedWindowRateLimiter } from "../src/lib/rate-limit";
3+
import type { NextRequest } from "next/server";
4+
5+
function makeRequest(headers: Record<string, string>): NextRequest {
6+
return {
7+
headers: {
8+
get: (name: string) => {
9+
const k = name.toLowerCase();
10+
for (const key of Object.keys(headers)) {
11+
if (key.toLowerCase() === k) return headers[key] ?? null;
12+
}
13+
return null;
14+
},
15+
},
16+
} as unknown as NextRequest;
17+
}
18+
19+
describe("getClientIp", () => {
20+
it("returns cf-connecting-ip when present", () => {
21+
const req = makeRequest({ "cf-connecting-ip": "1.2.3.4" });
22+
expect(getClientIp(req)).toBe("1.2.3.4");
23+
});
24+
25+
it("returns x-real-ip when cf-connecting-ip is absent", () => {
26+
const req = makeRequest({ "x-real-ip": "5.6.7.8" });
27+
expect(getClientIp(req)).toBe("5.6.7.8");
28+
});
29+
30+
it("returns first IP from x-forwarded-for when others are absent", () => {
31+
const req = makeRequest({ "x-forwarded-for": "10.0.0.1, 10.0.0.2, 10.0.0.3" });
32+
expect(getClientIp(req)).toBe("10.0.0.1");
33+
});
34+
35+
it("cf-connecting-ip takes priority over x-real-ip", () => {
36+
const req = makeRequest({ "cf-connecting-ip": "1.1.1.1", "x-real-ip": "2.2.2.2" });
37+
expect(getClientIp(req)).toBe("1.1.1.1");
38+
});
39+
40+
it("x-real-ip takes priority over x-forwarded-for", () => {
41+
const req = makeRequest({ "x-real-ip": "9.9.9.9", "x-forwarded-for": "1.1.1.1" });
42+
expect(getClientIp(req)).toBe("9.9.9.9");
43+
});
44+
45+
it("returns unknown when no headers present", () => {
46+
const req = makeRequest({});
47+
expect(getClientIp(req)).toBe("unknown");
48+
});
49+
50+
it("returns unknown when all IP headers are empty strings", () => {
51+
const req = makeRequest({ "cf-connecting-ip": "", "x-real-ip": "", "x-forwarded-for": "" });
52+
expect(getClientIp(req)).toBe("unknown");
53+
});
54+
55+
it("trims whitespace from IP values", () => {
56+
const req = makeRequest({ "cf-connecting-ip": " 1.2.3.4 " });
57+
expect(getClientIp(req)).toBe("1.2.3.4");
58+
});
59+
});
60+
61+
describe("createMemoryFixedWindowRateLimiter", () => {
62+
it("allows first request within limit", () => {
63+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 1000, maxEntries: 100 });
64+
const result = limiter.check("user-1", 5);
65+
expect(result.allowed).toBe(true);
66+
expect(result.remaining).toBe(4);
67+
});
68+
69+
it("denies requests after limit is reached", () => {
70+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 10000, maxEntries: 100 });
71+
for (let i = 0; i < 3; i++) {
72+
limiter.check("user-2", 3);
73+
}
74+
const result = limiter.check("user-2", 3);
75+
expect(result.allowed).toBe(false);
76+
expect(result.remaining).toBe(0);
77+
});
78+
79+
it("resets after the window expires", () => {
80+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 50, pruneIntervalMs: 10, maxEntries: 100 });
81+
limiter.check("user-3", 2);
82+
limiter.check("user-3", 2);
83+
// After window expires (now + 100ms), a new request should be allowed
84+
const result = limiter.check("user-3", 2, Date.now() + 100);
85+
expect(result.allowed).toBe(true);
86+
expect(result.remaining).toBe(1);
87+
});
88+
89+
it("resets per-key independently", () => {
90+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 10000, maxEntries: 100 });
91+
limiter.check("user-4a", 1);
92+
limiter.check("user-4a", 1);
93+
// user-4b should still be allowed
94+
const result = limiter.check("user-4b", 1);
95+
expect(result.allowed).toBe(true);
96+
expect(result.remaining).toBe(0);
97+
});
98+
99+
it("allows request again after the window expires", () => {
100+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 50, pruneIntervalMs: 10, maxEntries: 100 });
101+
const t0 = Date.now();
102+
limiter.check("user-5", 1, t0);
103+
limiter.check("user-5", 1, t0 + 100);
104+
// Should be allowed again since the bucket expired (resetAt = t0 + 50, now = t0 + 200)
105+
const result = limiter.check("user-5", 1, t0 + 200);
106+
expect(result.allowed).toBe(true);
107+
});
108+
109+
it("returns correct reset unix timestamp", () => {
110+
const limiter = createMemoryFixedWindowRateLimiter({ windowMs: 5000, maxEntries: 100 });
111+
const now = Date.now();
112+
const result = limiter.check("user-6", 3, now);
113+
expect(result.reset).toBe(Math.ceil((now + 5000) / 1000));
114+
});
115+
});

0 commit comments

Comments
 (0)