import { RateLimiter } from '../rate-limiter/rate-limiter'; import { RateLimitTimeoutError } from '../utils/error'; // Tests require Upstash Redis — skip if env vars not set const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL; const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN; const describeWithRedis = UPSTASH_URL && UPSTASH_TOKEN ? describe : describe.skip; describeWithRedis('RateLimiter (integration)', () => { let rateLimiter: RateLimiter; beforeEach(() => { rateLimiter = new RateLimiter({ redis: { url: UPSTASH_URL!, token: UPSTASH_TOKEN! }, hostId: 'test-host', timeoutMs: 2000, safetyMargin: 1.0, }); }); afterEach(async () => { await rateLimiter.cleanup('test-host'); }); it('allows requests under the limit', async () => { await expect(rateLimiter.acquire('GET', '/properties')).resolves.not.toThrow(); }); it('throws RateLimitTimeoutError when endpoint limit exceeded', async () => { const limiter = new RateLimiter({ redis: { url: UPSTASH_URL!, token: UPSTASH_TOKEN! }, hostId: 'test-host', timeoutMs: 500, safetyMargin: 1.0, }); // Conversations limit: 5 per 5 seconds for (let i = 0; i < 5; i++) { await limiter.acquire('POST', `/conversations/id${i}`); } // 6th should timeout await expect(limiter.acquire('POST', '/conversations/id6')) .rejects.toThrow(RateLimitTimeoutError); }); }); describe('RateLimiter (fail-closed)', () => { it('throws RateLimitTimeoutError when Redis is unavailable', async () => { const badLimiter = new RateLimiter({ redis: { url: 'https://fake-url.upstash.io', token: 'fake-token' }, hostId: 'test-host', timeoutMs: 1000, }); // Should throw — fails closed to prevent unthrottled requests await expect(badLimiter.acquire('GET', '/properties')) .rejects.toThrow(RateLimitTimeoutError); }); }); describe('RateLimiter (constructor)', () => { it('accepts RateLimiterConfig', () => { expect(() => { new RateLimiter({ redis: { url: 'https://fake.upstash.io', token: 'fake' }, hostId: 'test', }); }).not.toThrow(); }); });