removed old tests, created new ones and format
This commit is contained in:
parent
7579afa3c3
commit
b03231b849
57 changed files with 4092 additions and 5901 deletions
|
|
@ -1,311 +0,0 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import Redis from 'ioredis';
|
||||
import { QueueRateLimiter } from '../src/rate-limiter';
|
||||
import { getRedisConnection } from '../src/utils';
|
||||
|
||||
// Suppress Redis connection errors in tests
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
if (reason && typeof reason === 'object' && 'message' in reason) {
|
||||
const message = (reason as Error).message;
|
||||
if (
|
||||
message.includes('Connection is closed') ||
|
||||
message.includes('Connection is in monitoring mode')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('QueueRateLimiter', () => {
|
||||
let redisClient: Redis;
|
||||
let rateLimiter: QueueRateLimiter;
|
||||
|
||||
const redisConfig = {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
password: '',
|
||||
db: 0,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create Redis client
|
||||
redisClient = new Redis(getRedisConnection(redisConfig));
|
||||
|
||||
// Clear Redis keys for tests
|
||||
try {
|
||||
const keys = await redisClient.keys('rl:*');
|
||||
if (keys.length > 0) {
|
||||
await redisClient.del(...keys);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
rateLimiter = new QueueRateLimiter(redisClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (redisClient) {
|
||||
try {
|
||||
await redisClient.quit();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
describe('Rate Limit Rules', () => {
|
||||
test('should add and enforce global rate limit', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'global',
|
||||
config: {
|
||||
points: 5,
|
||||
duration: 1, // 1 second
|
||||
},
|
||||
});
|
||||
|
||||
// Consume 5 points
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const result = await rateLimiter.checkLimit('any-handler', 'any-operation');
|
||||
expect(result.allowed).toBe(true);
|
||||
}
|
||||
|
||||
// 6th request should be blocked
|
||||
const blocked = await rateLimiter.checkLimit('any-handler', 'any-operation');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
expect(blocked.retryAfter).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should add and enforce handler-level rate limit', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'api-handler',
|
||||
config: {
|
||||
points: 3,
|
||||
duration: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// api-handler should be limited
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = await rateLimiter.checkLimit('api-handler', 'any-operation');
|
||||
expect(result.allowed).toBe(true);
|
||||
}
|
||||
|
||||
const blocked = await rateLimiter.checkLimit('api-handler', 'any-operation');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
|
||||
// Other handlers should not be limited
|
||||
const otherHandler = await rateLimiter.checkLimit('other-handler', 'any-operation');
|
||||
expect(otherHandler.allowed).toBe(true);
|
||||
});
|
||||
|
||||
test('should add and enforce operation-level rate limit', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'operation',
|
||||
handler: 'data-handler',
|
||||
operation: 'fetch-prices',
|
||||
config: {
|
||||
points: 2,
|
||||
duration: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// Specific operation should be limited
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = await rateLimiter.checkLimit('data-handler', 'fetch-prices');
|
||||
expect(result.allowed).toBe(true);
|
||||
}
|
||||
|
||||
const blocked = await rateLimiter.checkLimit('data-handler', 'fetch-prices');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
|
||||
// Other operations on same handler should work
|
||||
const otherOp = await rateLimiter.checkLimit('data-handler', 'fetch-volume');
|
||||
expect(otherOp.allowed).toBe(true);
|
||||
});
|
||||
|
||||
test('should enforce multiple rate limits (most restrictive wins)', async () => {
|
||||
// Global: 10/sec
|
||||
rateLimiter.addRule({
|
||||
level: 'global',
|
||||
config: { points: 10, duration: 1 },
|
||||
});
|
||||
|
||||
// Handler: 5/sec
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'test-handler',
|
||||
config: { points: 5, duration: 1 },
|
||||
});
|
||||
|
||||
// Operation: 2/sec
|
||||
rateLimiter.addRule({
|
||||
level: 'operation',
|
||||
handler: 'test-handler',
|
||||
operation: 'test-op',
|
||||
config: { points: 2, duration: 1 },
|
||||
});
|
||||
|
||||
// Should be limited by operation level (most restrictive)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const result = await rateLimiter.checkLimit('test-handler', 'test-op');
|
||||
expect(result.allowed).toBe(true);
|
||||
}
|
||||
|
||||
const blocked = await rateLimiter.checkLimit('test-handler', 'test-op');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limit Status', () => {
|
||||
test('should get rate limit status', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'status-test',
|
||||
config: { points: 10, duration: 60 },
|
||||
});
|
||||
|
||||
// Consume some points
|
||||
await rateLimiter.checkLimit('status-test', 'operation');
|
||||
await rateLimiter.checkLimit('status-test', 'operation');
|
||||
|
||||
const status = await rateLimiter.getStatus('status-test', 'operation');
|
||||
expect(status.handler).toBe('status-test');
|
||||
expect(status.operation).toBe('operation');
|
||||
expect(status.limits.length).toBe(1);
|
||||
expect(status.limits[0].points).toBe(10);
|
||||
expect(status.limits[0].remaining).toBe(8);
|
||||
});
|
||||
|
||||
test('should show multiple applicable limits in status', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'global',
|
||||
config: { points: 100, duration: 60 },
|
||||
});
|
||||
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'multi-test',
|
||||
config: { points: 50, duration: 60 },
|
||||
});
|
||||
|
||||
const status = await rateLimiter.getStatus('multi-test', 'operation');
|
||||
expect(status.limits.length).toBe(2);
|
||||
|
||||
const globalLimit = status.limits.find(l => l.level === 'global');
|
||||
const handlerLimit = status.limits.find(l => l.level === 'handler');
|
||||
|
||||
expect(globalLimit?.points).toBe(100);
|
||||
expect(handlerLimit?.points).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limit Management', () => {
|
||||
test('should reset rate limits', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'reset-test',
|
||||
config: { points: 1, duration: 60 },
|
||||
});
|
||||
|
||||
// Consume the limit
|
||||
await rateLimiter.checkLimit('reset-test', 'operation');
|
||||
const blocked = await rateLimiter.checkLimit('reset-test', 'operation');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
|
||||
// Reset limits
|
||||
await rateLimiter.reset('reset-test');
|
||||
|
||||
// Should be allowed again
|
||||
const afterReset = await rateLimiter.checkLimit('reset-test', 'operation');
|
||||
expect(afterReset.allowed).toBe(true);
|
||||
});
|
||||
|
||||
test('should get all rules', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'global',
|
||||
config: { points: 100, duration: 60 },
|
||||
});
|
||||
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'test',
|
||||
config: { points: 50, duration: 60 },
|
||||
});
|
||||
|
||||
const rules = rateLimiter.getRules();
|
||||
expect(rules.length).toBe(2);
|
||||
expect(rules[0].level).toBe('global');
|
||||
expect(rules[1].level).toBe('handler');
|
||||
});
|
||||
|
||||
test('should remove specific rule', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'remove-test',
|
||||
config: { points: 1, duration: 1 },
|
||||
});
|
||||
|
||||
// Verify rule exists
|
||||
await rateLimiter.checkLimit('remove-test', 'op');
|
||||
const blocked = await rateLimiter.checkLimit('remove-test', 'op');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
|
||||
// Remove rule
|
||||
const removed = rateLimiter.removeRule('handler', 'remove-test');
|
||||
expect(removed).toBe(true);
|
||||
|
||||
// Should not be limited anymore
|
||||
const afterRemove = await rateLimiter.checkLimit('remove-test', 'op');
|
||||
expect(afterRemove.allowed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Block Duration', () => {
|
||||
test('should block for specified duration after limit exceeded', async () => {
|
||||
rateLimiter.addRule({
|
||||
level: 'handler',
|
||||
handler: 'block-test',
|
||||
config: {
|
||||
points: 1,
|
||||
duration: 1,
|
||||
blockDuration: 2, // Block for 2 seconds
|
||||
},
|
||||
});
|
||||
|
||||
// Consume limit
|
||||
await rateLimiter.checkLimit('block-test', 'op');
|
||||
|
||||
// Should be blocked
|
||||
const blocked = await rateLimiter.checkLimit('block-test', 'op');
|
||||
expect(blocked.allowed).toBe(false);
|
||||
expect(blocked.retryAfter).toBeGreaterThanOrEqual(1000); // At least 1 second
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should allow requests when rate limiter fails', async () => {
|
||||
// Create a rate limiter with invalid redis client
|
||||
const badRedis = new Redis({
|
||||
host: 'invalid-host',
|
||||
port: 9999,
|
||||
retryStrategy: () => null, // Disable retries
|
||||
});
|
||||
|
||||
const failingLimiter = new QueueRateLimiter(badRedis);
|
||||
|
||||
failingLimiter.addRule({
|
||||
level: 'global',
|
||||
config: { points: 1, duration: 1 },
|
||||
});
|
||||
|
||||
// Should allow even though Redis is not available
|
||||
const result = await failingLimiter.checkLimit('test', 'test');
|
||||
expect(result.allowed).toBe(true);
|
||||
|
||||
badRedis.disconnect();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue