Implement Redis rate limiting middleware for spam endpoints (FRE-4507)
- Add ioredis dependency to API package - Create Redis connection utility (apps/api/src/config/redis.ts) - Create Redis-backed spam rate limit middleware with per-minute and daily limits - Create spam classification routes (SMS, number reputation, call analysis, feedback) - Register middleware and routes in API server - Add 7 passing tests for rate limit enforcement - Update vitest config with required env vars Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
98
apps/api/src/__tests__/spam-rate-limit.test.ts
Normal file
98
apps/api/src/__tests__/spam-rate-limit.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||
import { RedisRateLimiter } from '../middleware/spam-rate-limit.middleware';
|
||||
import { redis } from '../config/redis';
|
||||
|
||||
describe('RedisRateLimiter', () => {
|
||||
const testKey = 'test-client';
|
||||
const limiter = new RedisRateLimiter();
|
||||
|
||||
beforeAll(async () => {
|
||||
await redis.connect();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await redis.quit();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await redis.del('spamshield:ratelimit:test-client');
|
||||
await redis.del('spamshield:ratelimit:daily:test-client');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await redis.del('spamshield:ratelimit:test-client');
|
||||
await redis.del('spamshield:ratelimit:daily:test-client');
|
||||
});
|
||||
|
||||
describe('checkLimit (per-minute)', () => {
|
||||
it('should allow requests within the limit', async () => {
|
||||
const result = await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
expect(result.remaining).toBe(9);
|
||||
expect(result.retryAfter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should decrement remaining on each request', async () => {
|
||||
const result1 = await limiter.checkLimit(testKey, 60, 10);
|
||||
const result2 = await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
expect(result1.remaining).toBe(9);
|
||||
expect(result2.remaining).toBe(8);
|
||||
});
|
||||
|
||||
it('should exceed limit after max requests', async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await limiter.checkLimit(testKey, 60, 10);
|
||||
}
|
||||
|
||||
const result = await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
expect(result.remaining).toBe(0);
|
||||
expect(result.retryAfter).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return retry-after when limit is exceeded', async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await limiter.checkLimit(testKey, 60, 10);
|
||||
}
|
||||
|
||||
const result = await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
expect(result.retryAfter).toBeGreaterThan(0);
|
||||
expect(result.retryAfter).toBeLessThanOrEqual(60000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkDailyLimit', () => {
|
||||
it('should allow requests within daily limit', async () => {
|
||||
const result = await limiter.checkDailyLimit(testKey, 100);
|
||||
|
||||
expect(result.remaining).toBe(99);
|
||||
expect(result.retryAfter).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should exceed daily limit after max requests', async () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await limiter.checkDailyLimit(testKey, 100);
|
||||
}
|
||||
|
||||
const result = await limiter.checkDailyLimit(testKey, 100);
|
||||
|
||||
expect(result.remaining).toBe(0);
|
||||
expect(result.retryAfter).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should clear the rate limit counter', async () => {
|
||||
await limiter.checkLimit(testKey, 60, 10);
|
||||
await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
await limiter.reset(testKey);
|
||||
|
||||
const result = await limiter.checkLimit(testKey, 60, 10);
|
||||
|
||||
expect(result.remaining).toBe(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user