mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Add ratelimiter tests
This commit is contained in:
parent
12de164a4f
commit
68a0ef6648
4 changed files with 116 additions and 1 deletions
31
src/utils/ratelimiter/MemoryRateLimiter.test.ts
Normal file
31
src/utils/ratelimiter/MemoryRateLimiter.test.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { assertEquals, assertThrows } from '@std/assert';
|
||||||
|
|
||||||
|
import { MemoryRateLimiter } from './MemoryRateLimiter.ts';
|
||||||
|
import { RateLimitError } from './RateLimitError.ts';
|
||||||
|
|
||||||
|
Deno.test('MemoryRateLimiter', async (t) => {
|
||||||
|
const limit = 5;
|
||||||
|
const window = 100;
|
||||||
|
|
||||||
|
using limiter = new MemoryRateLimiter({ limit, window });
|
||||||
|
|
||||||
|
await t.step('can hit up to limit', () => {
|
||||||
|
for (let i = 0; i < limit; i++) {
|
||||||
|
const client = limiter.client('test');
|
||||||
|
assertEquals(client.hits, i);
|
||||||
|
client.hit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('throws when hit if limit exceeded', () => {
|
||||||
|
assertThrows(() => limiter.client('test').hit(), RateLimitError);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('can hit after window resets', async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, window + 1));
|
||||||
|
|
||||||
|
const client = limiter.client('test');
|
||||||
|
assertEquals(client.hits, 0);
|
||||||
|
client.hit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -35,7 +35,7 @@ export class MemoryRateLimiter implements RateLimiter {
|
||||||
return curr;
|
return curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prev) {
|
if (prev && prev.resetAt > new Date()) {
|
||||||
this.current.set(key, prev);
|
this.current.set(key, prev);
|
||||||
this.previous.delete(key);
|
this.previous.delete(key);
|
||||||
return prev;
|
return prev;
|
||||||
|
|
|
||||||
39
src/utils/ratelimiter/MultiRateLimiter.test.ts
Normal file
39
src/utils/ratelimiter/MultiRateLimiter.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { assertEquals, assertThrows } from '@std/assert';
|
||||||
|
|
||||||
|
import { MemoryRateLimiter } from './MemoryRateLimiter.ts';
|
||||||
|
import { MultiRateLimiter } from './MultiRateLimiter.ts';
|
||||||
|
|
||||||
|
Deno.test('MultiRateLimiter', async (t) => {
|
||||||
|
using limiter1 = new MemoryRateLimiter({ limit: 5, window: 100 });
|
||||||
|
using limiter2 = new MemoryRateLimiter({ limit: 8, window: 200 });
|
||||||
|
|
||||||
|
const limiter = new MultiRateLimiter([limiter1, limiter2]);
|
||||||
|
|
||||||
|
await t.step('can hit up to first limit', () => {
|
||||||
|
for (let i = 0; i < limiter1.limit; i++) {
|
||||||
|
const client = limiter.client('test');
|
||||||
|
assertEquals(client.hits, i);
|
||||||
|
client.hit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('throws when hit if first limit exceeded', () => {
|
||||||
|
assertThrows(() => limiter.client('test').hit(), Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('can hit up to second limit after the first window resets', async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, limiter1.window + 1));
|
||||||
|
|
||||||
|
const limit = limiter2.limit - limiter1.limit - 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < limit; i++) {
|
||||||
|
const client = limiter.client('test');
|
||||||
|
assertEquals(client.hits, i);
|
||||||
|
client.hit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.step('throws when hit if second limit exceeded', () => {
|
||||||
|
assertThrows(() => limiter.client('test').hit(), Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
45
src/utils/ratelimiter/MultiRateLimiter.ts
Normal file
45
src/utils/ratelimiter/MultiRateLimiter.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { RateLimiter, RateLimiterClient } from './types.ts';
|
||||||
|
|
||||||
|
export class MultiRateLimiter {
|
||||||
|
constructor(private limiters: RateLimiter[]) {}
|
||||||
|
|
||||||
|
client(key: string): RateLimiterClient {
|
||||||
|
return new MultiRateLimiterClient(key, this.limiters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultiRateLimiterClient implements RateLimiterClient {
|
||||||
|
constructor(private key: string, private limiters: RateLimiter[]) {
|
||||||
|
if (!limiters.length) {
|
||||||
|
throw new Error('No limiters provided');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hits(): number {
|
||||||
|
return this.limiters[0].client(this.key).hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
get resetAt(): Date {
|
||||||
|
return this.limiters[0].client(this.key).resetAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
get remaining(): number {
|
||||||
|
return this.limiters[0].client(this.key).remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
hit(n?: number): void {
|
||||||
|
let error: unknown;
|
||||||
|
|
||||||
|
for (const limiter of this.limiters) {
|
||||||
|
try {
|
||||||
|
limiter.client(this.key).hit(n);
|
||||||
|
} catch (e) {
|
||||||
|
error ??= e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue