Skip to content

Commit 65413a4

Browse files
committed
refactor: consolidate test files and update middleware rate-limiting logic
1 parent aed644b commit 65413a4

8 files changed

Lines changed: 114 additions & 231 deletions

hooks/SuggestRepoModal.tsx)

Whitespace-only changes.

hooks/useDebounce.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,63 @@ describe('useDebounce', () => {
4242
vi.useRealTimers();
4343
}
4444
});
45+
46+
it('updates only after the delay', () => {
47+
vi.useFakeTimers();
48+
try {
49+
const { result, rerender } = renderHook(({ value }) => useDebounce(value, 400), {
50+
initialProps: { value: 'a' },
51+
});
52+
53+
expect(result.current).toBe('a');
54+
55+
rerender({ value: 'ab' });
56+
rerender({ value: 'abc' });
57+
58+
act(() => {
59+
vi.advanceTimersByTime(399);
60+
});
61+
62+
expect(result.current).toBe('a');
63+
64+
act(() => {
65+
vi.advanceTimersByTime(1);
66+
});
67+
68+
expect(result.current).toBe('abc');
69+
} finally {
70+
vi.useRealTimers();
71+
}
72+
});
73+
74+
it('resets the timer when value changes repeatedly', () => {
75+
vi.useFakeTimers();
76+
try {
77+
const { result, rerender } = renderHook(({ value }) => useDebounce(value, 400), {
78+
initialProps: { value: 'a' },
79+
});
80+
81+
rerender({ value: 'ab' });
82+
83+
act(() => {
84+
vi.advanceTimersByTime(200);
85+
});
86+
87+
rerender({ value: 'abc' });
88+
89+
act(() => {
90+
vi.advanceTimersByTime(200);
91+
});
92+
93+
expect(result.current).toBe('a');
94+
95+
act(() => {
96+
vi.advanceTimersByTime(200);
97+
});
98+
99+
expect(result.current).toBe('abc');
100+
} finally {
101+
vi.useRealTimers();
102+
}
103+
});
45104
});

lib/hooks/useDebounce.test.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

lib/hooks/useDebounce.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

middleware.ratelimit.test.ts

Lines changed: 0 additions & 76 deletions
This file was deleted.

middleware.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
22
import { NextRequest, NextResponse } from 'next/server';
3-
import { middleware } from './middleware';
3+
import { middleware, config } from './middleware';
44
import { rateLimit } from '@/lib/rate-limit';
55

66
vi.mock('@/lib/rate-limit', () => ({
@@ -67,7 +67,7 @@ describe('middleware', () => {
6767
});
6868
});
6969

70-
it('sets X-RateLimit-Limit header when rate limit succeeds', async () => {
70+
it('calls rateLimit with fixed policy values (60 requests / 60000ms)', async () => {
7171
vi.mocked(rateLimit).mockResolvedValue({
7272
success: true,
7373
limit: 60,
@@ -76,12 +76,12 @@ describe('middleware', () => {
7676
});
7777

7878
const request = new NextRequest('http://localhost:3000/api/streak?user=octocat');
79-
const response = await middleware(request);
79+
await middleware(request);
8080

81-
expect(response.headers.get('X-RateLimit-Limit')).toBe('60');
81+
expect(rateLimit).toHaveBeenCalledWith(expect.any(String), 60, 60000);
8282
});
8383

84-
it('sets X-RateLimit-Remaining header when rate limit succeeds', async () => {
84+
it('sets all X-RateLimit headers on successful requests', async () => {
8585
vi.mocked(rateLimit).mockResolvedValue({
8686
success: true,
8787
limit: 60,
@@ -92,7 +92,26 @@ describe('middleware', () => {
9292
const request = new NextRequest('http://localhost:3000/api/streak?user=octocat');
9393
const response = await middleware(request);
9494

95+
expect(response.headers.get('X-RateLimit-Limit')).toBe('60');
9596
expect(response.headers.get('X-RateLimit-Remaining')).toBe('59');
97+
expect(response.headers.get('X-RateLimit-Reset')).toBe('123456789');
98+
});
99+
100+
it('sets JSON and rate headers on throttled responses', async () => {
101+
vi.mocked(rateLimit).mockResolvedValue({
102+
success: false,
103+
limit: 60,
104+
remaining: 0,
105+
reset: 123456789,
106+
});
107+
108+
const request = new NextRequest('http://localhost:3000/api/streak?user=octocat');
109+
const response = await middleware(request);
110+
111+
expect(response.headers.get('Content-Type')).toBe('application/json');
112+
expect(response.headers.get('X-RateLimit-Limit')).toBe('60');
113+
expect(response.headers.get('X-RateLimit-Remaining')).toBe('0');
114+
expect(response.headers.get('X-RateLimit-Reset')).toBe('123456789');
96115
});
97116

98117
it('uses first IP from x-forwarded-for when subsequent hops are trusted', async () => {
@@ -209,4 +228,8 @@ describe('middleware', () => {
209228

210229
expect(rateLimit).toHaveBeenCalledWith('1.2.3.4', 60, 60000);
211230
});
231+
232+
it('includes compare API matcher in middleware config', () => {
233+
expect(config.matcher).toContain('/api/compare/:path*');
234+
});
212235
});

utils/getClientIp.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,31 @@ describe('getClientIp', () => {
9696
});
9797
expect(ip).toBe('198.51.100.5');
9898
});
99+
100+
it('sanitizes multi-IP lists and traverses untrusted hops from right to left', () => {
101+
const req = new Request('http://localhost:3000/api/streak', {
102+
headers: {
103+
'x-forwarded-for': '203.0.113.195, 198.51.100.10, 192.168.1.1',
104+
},
105+
});
106+
const options = {
107+
proxyConfig: {
108+
trustedProxies: ['192.168.1.1'],
109+
trustPrivateRanges: false,
110+
},
111+
};
112+
expect(getClientIp(req, options)).toBe('198.51.100.10');
113+
});
114+
115+
it('falls back to 127.0.0.1 when headers are missing or malformed', () => {
116+
const req = new Request('http://localhost:3000/api/streak');
117+
expect(getClientIp(req)).toBe('127.0.0.1');
118+
119+
const reqMalformed = new Request('http://localhost:3000/api/streak', {
120+
headers: {
121+
'x-forwarded-for': ' , ',
122+
},
123+
});
124+
expect(getClientIp(reqMalformed)).toBe('127.0.0.1');
125+
});
99126
});

utils/trustedProxy.test.ts

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -31,76 +31,3 @@ describe('isTrustedProxy and Helpers', () => {
3131
expect(isTrustedProxy('8.8.8.8', config)).toBe(false);
3232
});
3333
});
34-
35-
describe('getClientIp extraction and fallback', () => {
36-
// Test Case 1: extraction of IP from single X-Forwarded-For header
37-
it('extracts IP from a single X-Forwarded-For header when proxy is trusted', () => {
38-
const req = new Request('http://localhost', {
39-
headers: {
40-
'x-forwarded-for': '203.0.113.195',
41-
},
42-
});
43-
const options = {
44-
proxyConfig: {
45-
trustedProxies: ['*'],
46-
trustPrivateRanges: false,
47-
},
48-
};
49-
expect(getClientIp(req, options)).toBe('203.0.113.195');
50-
});
51-
52-
// Test Case 2: sanitization of multi-IP lists
53-
it('sanitizes multi-IP lists and traverses untrusted hops from right to left', () => {
54-
const req = new Request('http://localhost', {
55-
headers: {
56-
'x-forwarded-for': '203.0.113.195, 198.51.100.10, 192.168.1.1',
57-
},
58-
});
59-
const options = {
60-
proxyConfig: {
61-
trustedProxies: ['192.168.1.1'],
62-
trustPrivateRanges: false,
63-
},
64-
};
65-
expect(getClientIp(req, options)).toBe('198.51.100.10');
66-
});
67-
68-
// Test Case 3: Cloudflare Cf-Connecting-Ip has precedence
69-
it('gives precedence to priority headers like cf-connecting-ip', () => {
70-
const req = new Request('http://localhost', {
71-
headers: {
72-
'cf-connecting-ip': '203.0.113.5',
73-
'x-real-ip': '198.51.100.20',
74-
},
75-
});
76-
const options = {
77-
headersPriority: ['cf-connecting-ip', 'x-real-ip'],
78-
proxyConfig: {
79-
trustedProxies: [],
80-
trustPrivateRanges: false,
81-
},
82-
};
83-
expect(getClientIp(req, options)).toBe('203.0.113.5');
84-
});
85-
86-
// Test Case 4: fallback to connection remoteAddress (request.ip)
87-
it('falls back to request.ip representing connection remoteAddress', () => {
88-
const req = new NextRequest('http://localhost');
89-
Object.defineProperty(req, 'ip', { value: '198.51.100.5', configurable: true });
90-
91-
expect(getClientIp(req)).toBe('198.51.100.5');
92-
});
93-
94-
// Test Case 5: behavior when headers are missing or malformed
95-
it('falls back to 127.0.0.1 when headers are missing or malformed', () => {
96-
const req = new Request('http://localhost');
97-
expect(getClientIp(req)).toBe('127.0.0.1');
98-
99-
const reqMalformed = new Request('http://localhost', {
100-
headers: {
101-
'x-forwarded-for': ' , ',
102-
},
103-
});
104-
expect(getClientIp(reqMalformed)).toBe('127.0.0.1');
105-
});
106-
});

0 commit comments

Comments
 (0)