|
1 | | -import { beforeEach, describe, expect, it, vi } from 'vitest'; |
| 1 | +import { beforeEach, describe, expect, it } from 'vitest'; |
2 | 2 | import refreshRateLimiter from './refresh-rate-limiter'; |
3 | 3 |
|
4 | | -function setViewportWidth(width: number) { |
5 | | - Object.defineProperty(window, 'innerWidth', { |
6 | | - configurable: true, |
7 | | - writable: true, |
8 | | - value: width, |
9 | | - }); |
10 | | - window.dispatchEvent(new Event('resize')); |
11 | | -} |
12 | | - |
13 | | -function mockMatchMedia(matches: boolean) { |
14 | | - Object.defineProperty(window, 'matchMedia', { |
15 | | - configurable: true, |
16 | | - writable: true, |
17 | | - value: vi.fn().mockImplementation((query: string) => ({ |
18 | | - matches, |
19 | | - media: query, |
20 | | - onchange: null, |
21 | | - addListener: vi.fn(), |
22 | | - removeListener: vi.fn(), |
23 | | - addEventListener: vi.fn(), |
24 | | - removeEventListener: vi.fn(), |
25 | | - dispatchEvent: vi.fn(), |
26 | | - })), |
27 | | - }); |
28 | | -} |
29 | | - |
30 | 4 | describe('RefreshRateLimiter responsive breakpoints', () => { |
31 | 5 | beforeEach(() => { |
32 | 6 | refreshRateLimiter.reset(); |
33 | | - setViewportWidth(1280); |
34 | | - mockMatchMedia(false); |
| 7 | + delete process.env.MAX_REFRESHES_PER_HOUR; |
| 8 | + window.innerWidth = 375; |
| 9 | + window.dispatchEvent(new Event('resize')); |
35 | 10 | }); |
36 | 11 |
|
37 | | - it('allows refreshes normally on a mobile-width viewport (375px)', () => { |
38 | | - setViewportWidth(375); |
39 | | - mockMatchMedia(true); |
| 12 | + it('maintains default limit behavior under a narrow mobile viewport', () => { |
| 13 | + expect(window.innerWidth).toBe(375); |
40 | 14 |
|
41 | | - const response = refreshRateLimiter.checkLimit('203.0.113.5'); |
| 15 | + const response1 = refreshRateLimiter.checkLimit('203.0.113.5'); |
| 16 | + expect(response1.success).toBe(true); |
| 17 | + expect(response1.limit).toBe(3); |
| 18 | + expect(response1.remaining).toBe(2); |
42 | 19 |
|
43 | | - expect(window.innerWidth).toBe(375); |
44 | | - expect(response.success).toBe(true); |
45 | | - expect(response.limit).toBe(3); |
46 | | - expect(response.remaining).toBe(2); |
| 20 | + const response2 = refreshRateLimiter.checkLimit('203.0.113.5'); |
| 21 | + expect(response2.success).toBe(true); |
| 22 | + expect(response2.remaining).toBe(1); |
47 | 23 | }); |
48 | 24 |
|
49 | | - it('maintains the same rate-limit behavior when switching from desktop to mobile widths', () => { |
50 | | - setViewportWidth(1280); |
51 | | - const desktopResponse = refreshRateLimiter.checkLimit('198.51.100.10'); |
| 25 | + it('reflows client tracking correctly when the same IP is reused on mobile breakpoints', () => { |
| 26 | + window.innerWidth = 375; |
| 27 | + window.dispatchEvent(new Event('resize')); |
52 | 28 |
|
53 | | - setViewportWidth(375); |
54 | | - mockMatchMedia(true); |
55 | | - const mobileResponse = refreshRateLimiter.checkLimit('198.51.100.10'); |
| 29 | + const first = refreshRateLimiter.checkLimit(' 203.0.113.5 '); |
| 30 | + expect(first.success).toBe(true); |
| 31 | + expect(first.remaining).toBe(2); |
56 | 32 |
|
57 | | - expect(window.innerWidth).toBe(375); |
58 | | - expect(desktopResponse.success).toBe(true); |
59 | | - expect(mobileResponse.success).toBe(true); |
60 | | - expect(mobileResponse.remaining).toBe(desktopResponse.remaining - 1); |
| 33 | + const second = refreshRateLimiter.checkLimit('203.0.113.5'); |
| 34 | + expect(second.success).toBe(true); |
| 35 | + expect(second.remaining).toBe(1); |
| 36 | + expect(second.limit).toBe(first.limit); |
61 | 37 | }); |
62 | 38 |
|
63 | | - it('reports cleanly when a mobile breakpoint is active and the same IP checks again', () => { |
64 | | - setViewportWidth(375); |
65 | | - mockMatchMedia(true); |
| 39 | + it('adapts to environment-driven limits while preserving mobile-friendly fallback state', () => { |
| 40 | + process.env.MAX_REFRESHES_PER_HOUR = '2'; |
| 41 | + refreshRateLimiter.reset(); |
66 | 42 |
|
67 | | - const first = refreshRateLimiter.checkLimit('127.0.0.1'); |
68 | | - const second = refreshRateLimiter.checkLimit('127.0.0.1'); |
| 43 | + expect(window.innerWidth).toBe(375); |
69 | 44 |
|
| 45 | + const first = refreshRateLimiter.checkLimit('198.51.100.1'); |
70 | 46 | expect(first.success).toBe(true); |
| 47 | + expect(first.limit).toBe(2); |
| 48 | + expect(first.remaining).toBe(1); |
| 49 | + |
| 50 | + const second = refreshRateLimiter.checkLimit('198.51.100.1'); |
71 | 51 | expect(second.success).toBe(true); |
72 | | - expect(second.remaining).toBe(1); |
73 | | - expect(first.limit).toBe(3); |
| 52 | + expect(second.remaining).toBe(0); |
74 | 53 | }); |
75 | 54 |
|
76 | | - it('enforces a custom mobile friendly limit without breaking on narrow viewports', () => { |
77 | | - setViewportWidth(375); |
78 | | - mockMatchMedia(true); |
79 | | - refreshRateLimiter.setLimit(1, 60 * 60 * 1000); |
| 55 | + it('returns a reset timestamp without relying on absolute screen dimensions', () => { |
| 56 | + const response = refreshRateLimiter.checkLimit('198.51.100.2'); |
| 57 | + expect(response.reset).toBeGreaterThan(Date.now()); |
| 58 | + expect(typeof response.reset).toBe('number'); |
| 59 | + expect(response.limit).toBe(3); |
| 60 | + }); |
80 | 61 |
|
81 | | - const allowed = refreshRateLimiter.checkLimit('203.0.113.15'); |
82 | | - const blocked = refreshRateLimiter.checkLimit('203.0.113.15'); |
| 62 | + it('enforces the limit and reports blocked refreshes cleanly on simulated mobile width', () => { |
| 63 | + refreshRateLimiter.setLimit(1, 1000 * 60 * 60); |
| 64 | + expect(window.innerWidth).toBe(375); |
83 | 65 |
|
| 66 | + const allowed = refreshRateLimiter.checkLimit('203.0.113.10'); |
84 | 67 | expect(allowed.success).toBe(true); |
85 | 68 | expect(allowed.remaining).toBe(0); |
| 69 | + |
| 70 | + const blocked = refreshRateLimiter.checkLimit('203.0.113.10'); |
86 | 71 | expect(blocked.success).toBe(false); |
87 | 72 | expect(blocked.remaining).toBe(0); |
88 | 73 | expect(blocked.limit).toBe(1); |
89 | 74 | }); |
90 | | - |
91 | | - it('keeps responsive viewport simulation clean and does not depend on absolute width assumptions', () => { |
92 | | - setViewportWidth(375); |
93 | | - mockMatchMedia(true); |
94 | | - |
95 | | - const response = refreshRateLimiter.checkLimit('192.0.2.1'); |
96 | | - |
97 | | - expect(window.innerWidth).toBeLessThanOrEqual(375); |
98 | | - expect(response.success).toBe(true); |
99 | | - expect(response.reset).toBeGreaterThan(Date.now() - 1000); |
100 | | - expect(response.limit).toBe(3); |
101 | | - }); |
102 | 75 | }); |
0 commit comments