Skip to content

Commit 18394e6

Browse files
Merge branch 'JhaSourav07:main' into main
2 parents 5163f7f + 6ede310 commit 18394e6

7 files changed

Lines changed: 69 additions & 36 deletions

File tree

app/customize/components/ThemeSelector.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { render, screen } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import { ThemeSelector } from './ThemeSelector';
55
import { THEME_KEYS } from '../types';
6-
import userEvent from '@testing-library/user-event';
76

87
describe('ThemeSelector', () => {
98
const onThemeChange = vi.fn();

lib/rate-limit.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ describe('rateLimit', () => {
108108
});
109109
});
110110

111+
it('keys expire exactly at the window limit with sliding time advances', async () => {
112+
vi.useFakeTimers();
113+
const ip = '9.9.9.9';
114+
const windowMs = 1000;
115+
const limit = 5;
116+
117+
// First request: creates the tracker with 1s TTL
118+
let res = await rateLimit(ip, limit, windowMs);
119+
expect(res.success).toBe(true);
120+
expect(res.remaining).toBe(limit - 1);
121+
122+
// Advance half the window and make another request
123+
vi.advanceTimersByTime(500);
124+
res = await rateLimit(ip, limit, windowMs);
125+
expect(res.success).toBe(true);
126+
127+
// Advance to exactly the original window boundary (total = 1000ms)
128+
vi.advanceTimersByTime(500);
129+
130+
// At the exact boundary the entry should still be considered valid
131+
res = await rateLimit(ip, limit, windowMs);
132+
expect(res.success).toBe(true);
133+
134+
// Move just past the window expiry
135+
vi.advanceTimersByTime(1);
136+
137+
// Now the key must have expired and a fresh window starts
138+
res = await rateLimit(ip, limit, windowMs);
139+
expect(res.success).toBe(true);
140+
expect(res.remaining).toBe(limit - 1);
141+
});
142+
111143
it('allows requests after many expired IP entries', async () => {
112144
const windowMs = 1000;
113145

lib/rate-limit.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,24 @@ export class RateLimiter {
107107
async reset(ip: string): Promise<void> {
108108
await this.cache.delete(ip);
109109
}
110+
/**
111+
* Returns the number of remaining requests allowed for a given IP
112+
* in the current window.
113+
*
114+
* Does not consume a request — use `check()` for that.
115+
*
116+
* @param ip - The IP address to check.
117+
* @returns Promise resolving to the number of remaining requests,
118+
* or the full limit if the IP has no recorded requests.
119+
*
120+
* @example
121+
* const left = await rateLimiter.remaining("192.168.1.1");
122+
* console.log(`You have ${left} requests left.`);
123+
*/
124+
async remaining(ip: string): Promise<number> {
125+
const current = (await this.cache.get(ip)) ?? 0;
126+
return Math.max(0, this.limit - current);
127+
}
110128

111129
allow(ip: string): void {
112130
this.allowlist.add(ip);

lib/validations.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -552,16 +552,15 @@ describe('streakParamsSchema — boolean transform fields', () => {
552552
});
553553

554554
// ── hide_background ────────────────────────────────────────────────────────
555-
// Stricter than hide_title/hide_stats — only exact 'true' is accepted,
556-
// '1' does NOT enable it.
555+
// Same dual-value rule as hide_title/hide_stats: 'true' and '1' are both truthy.
557556

558557
describe('hide_background', () => {
559558
it('returns true when hide_background="true"', () => {
560559
expect(parse({ hide_background: 'true' }).hide_background).toBe(true);
561560
});
562561

563-
it('returns false when hide_background="1" (only exact "true" accepted)', () => {
564-
expect(parse({ hide_background: '1' }).hide_background).toBe(false);
562+
it('returns true when hide_background="1" (both "true" and "1" accepted)', () => {
563+
expect(parse({ hide_background: '1' }).hide_background).toBe(true);
565564
});
566565

567566
it('returns false when hide_background="false"', () => {

lib/validations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const streakParamsSchema = z.object({
171171
),
172172
refresh: z.string().optional().transform(toRefreshFlag),
173173
hide_title: z.string().optional().transform(toBooleanFlag),
174-
hide_background: z.string().optional().transform(toRefreshFlag),
174+
hide_background: z.string().optional().transform(toBooleanFlag),
175175
hide_stats: z.string().optional().transform(toBooleanFlag),
176176
lang: z.string().optional().default('en'),
177177
// Unknown view values fall back to the default dashboard view.

package-lock.json

Lines changed: 11 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/tracking.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,18 @@ describe('trackUser', () => {
8989

9090
it('handles empty username without crashing', () => {
9191
const sendBeaconMock = vi.fn().mockReturnValue(true);
92+
const fetchMock = vi.fn();
9293

9394
Object.defineProperty(navigator, 'sendBeacon', {
9495
value: sendBeaconMock,
9596
configurable: true,
9697
});
9798

99+
vi.stubGlobal('fetch', fetchMock);
100+
98101
expect(() => trackUser('')).not.toThrow();
99102
expect(sendBeaconMock).not.toHaveBeenCalled();
103+
expect(fetchMock).not.toHaveBeenCalled();
100104
});
101105

102106
it('falls back to fetch when sendBeacon returns false', () => {

0 commit comments

Comments
 (0)