馃 distributed transcription service
thistle.dunkirk.sh
1import { expect, test } from "bun:test";
2import db from "../db/schema";
3import { checkRateLimit, cleanupOldAttempts } from "./rate-limit";
4
5// Clean up before tests
6db.run("DELETE FROM rate_limit_attempts");
7
8test("allows requests under the limit", () => {
9 const key = "test:allow";
10
11 const result1 = checkRateLimit(key, 5, 60);
12 expect(result1.allowed).toBe(true);
13
14 const result2 = checkRateLimit(key, 5, 60);
15 expect(result2.allowed).toBe(true);
16
17 const result3 = checkRateLimit(key, 5, 60);
18 expect(result3.allowed).toBe(true);
19});
20
21test("blocks requests over the limit", () => {
22 const key = "test:block";
23
24 // Make 5 requests (limit)
25 for (let i = 0; i < 5; i++) {
26 const result = checkRateLimit(key, 5, 60);
27 expect(result.allowed).toBe(true);
28 }
29
30 // 6th request should be blocked
31 const blocked = checkRateLimit(key, 5, 60);
32 expect(blocked.allowed).toBe(false);
33 expect(blocked.retryAfter).toBeGreaterThan(0);
34});
35
36test("rolling window allows requests after time passes", async () => {
37 const key = "test:rolling";
38
39 // Make 3 requests
40 for (let i = 0; i < 3; i++) {
41 checkRateLimit(key, 3, 2); // 3 per 2 seconds
42 }
43
44 // 4th should be blocked
45 let result = checkRateLimit(key, 3, 2);
46 expect(result.allowed).toBe(false);
47
48 // Wait for window to pass
49 await new Promise((resolve) => setTimeout(resolve, 2100));
50
51 // Should now be allowed (old attempts outside window)
52 result = checkRateLimit(key, 3, 2);
53 expect(result.allowed).toBe(true);
54});
55
56test("different keys are tracked separately", () => {
57 const key1 = "test:separate1";
58 const key2 = "test:separate2";
59
60 // Exhaust limit for key1
61 for (let i = 0; i < 5; i++) {
62 checkRateLimit(key1, 5, 60);
63 }
64
65 const blocked = checkRateLimit(key1, 5, 60);
66 expect(blocked.allowed).toBe(false);
67
68 // key2 should still be allowed
69 const allowed = checkRateLimit(key2, 5, 60);
70 expect(allowed.allowed).toBe(true);
71});
72
73test("cleanup removes old attempts", () => {
74 const key = "test:cleanup";
75
76 // Create some attempts
77 checkRateLimit(key, 10, 60);
78 checkRateLimit(key, 10, 60);
79
80 // Manually insert an old attempt (25 hours ago)
81 const oldTimestamp = Math.floor(Date.now() / 1000) - 25 * 60 * 60;
82 db.run("INSERT INTO rate_limit_attempts (key, timestamp) VALUES (?, ?)", [
83 "test:old",
84 oldTimestamp,
85 ]);
86
87 // Run cleanup (default: removes attempts older than 24 hours)
88 cleanupOldAttempts();
89
90 // Old attempt should be gone
91 const count = db
92 .query<{ count: number }, []>(
93 "SELECT COUNT(*) as count FROM rate_limit_attempts WHERE key = 'test:old'",
94 )
95 .get();
96
97 expect(count?.count).toBe(0);
98
99 // Recent attempts should still exist
100 const recentCount = db
101 .query<{ count: number }, [string]>(
102 "SELECT COUNT(*) as count FROM rate_limit_attempts WHERE key = ?",
103 )
104 .get(key);
105
106 expect(recentCount?.count).toBe(2);
107});
108
109// Cleanup after tests
110db.run("DELETE FROM rate_limit_attempts");