Skip to main content
Glama
rateLimiter.test.ts13.1 kB
import request from 'supertest'; import express from 'express'; import { createRateLimiter, strictRateLimiter, standardRateLimiter, lenientRateLimiter, tallyApiLimiter, tallyApiRateLimit, createCompositeRateLimiter, rateLimitErrorHandler, getRateLimitStatus, rateLimitConfigs } from '../rateLimiter'; // Note: Redis mocking removed as Redis store is not currently implemented describe('Rate Limiter Middleware', () => { let app: express.Application; beforeEach(() => { app = express(); app.use(express.json()); // Reset the Tally API rate limiter (tallyApiLimiter as any).tokens = 100; (tallyApiLimiter as any).lastRefill = Date.now(); }); describe('Basic Rate Limiting', () => { it('should allow requests within the limit', async () => { const rateLimiter = createRateLimiter({ windowMs: 60000, max: 5, message: { error: 'Too many requests', retryAfter: '1 minute' }, standardHeaders: true, legacyHeaders: false }); app.use(rateLimiter); app.get('/test', (req, res) => { res.json({ success: true }); }); // Make requests within the limit for (let i = 0; i < 5; i++) { const response = await request(app) .get('/test') .expect(200); expect(response.body).toEqual({ success: true }); // Check for rate limit headers (express-rate-limit uses standard headers) const limitHeader = response.headers['x-ratelimit-limit'] || response.headers['ratelimit-limit']; const remainingHeader = response.headers['x-ratelimit-remaining'] || response.headers['ratelimit-remaining']; expect(limitHeader).toBe('5'); expect(remainingHeader).toBe((4 - i).toString()); } }); it('should block requests that exceed the limit', async () => { const rateLimiter = createRateLimiter({ windowMs: 60000, max: 2, message: { error: 'Too many requests', retryAfter: '1 minute' }, standardHeaders: true, legacyHeaders: false }); app.use(rateLimiter); app.get('/test', (req, res) => { res.json({ success: true }); }); // Make requests up to the limit await request(app).get('/test').expect(200); await request(app).get('/test').expect(200); // Next request should be rate limited const response = await request(app) .get('/test') .expect(429); expect(response.body.error).toBe('Too many requests'); }); it('should skip rate limiting for health checks', async () => { const rateLimiter = createRateLimiter({ windowMs: 60000, max: 1, message: { error: 'Too many requests', retryAfter: '1 minute' }, standardHeaders: true, legacyHeaders: false }); app.use(rateLimiter); app.get('/health', (req, res) => { res.json({ status: 'ok' }); }); app.get('/test', (req, res) => { res.json({ success: true }); }); // Health check should not be rate limited await request(app).get('/health').expect(200); await request(app).get('/health').expect(200); await request(app).get('/health').expect(200); // Regular endpoint should still be rate limited await request(app).get('/test').expect(200); await request(app).get('/test').expect(429); }); }); describe('Pre-configured Rate Limiters', () => { it('should apply strict rate limiting', async () => { app.use('/strict', strictRateLimiter); app.get('/strict/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/strict/test') .expect(200); const limitHeader = response.headers['x-ratelimit-limit'] || response.headers['ratelimit-limit']; expect(limitHeader).toBe('10'); }); it('should apply standard rate limiting', async () => { app.use('/standard', standardRateLimiter); app.get('/standard/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/standard/test') .expect(200); const limitHeader = response.headers['x-ratelimit-limit'] || response.headers['ratelimit-limit']; expect(limitHeader).toBe('100'); }); it('should apply lenient rate limiting', async () => { app.use('/lenient', lenientRateLimiter); app.get('/lenient/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/lenient/test') .expect(200); const limitHeader = response.headers['x-ratelimit-limit'] || response.headers['ratelimit-limit']; expect(limitHeader).toBe('1000'); }); }); describe('Tally API Rate Limiter', () => { it('should allow requests when tokens are available', async () => { app.use(tallyApiRateLimit); app.get('/tally', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/tally') .expect(200); expect(response.body).toEqual({ success: true }); expect(response.headers['x-tally-ratelimit-limit']).toBe('100'); expect(response.headers['x-tally-ratelimit-remaining']).toBe('99'); }); it('should block requests when no tokens are available', async () => { // Exhaust all tokens (tallyApiLimiter as any).tokens = 0; app.use(tallyApiRateLimit); app.get('/tally', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/tally') .expect(429); expect(response.body.error).toBe('Tally API rate limit exceeded'); expect(response.body.tokensRemaining).toBe(0); expect(response.body.retryAfter).toBeGreaterThan(0); }); it('should refill tokens over time', async () => { // Set tokens to 0 and move time forward (tallyApiLimiter as any).tokens = 0; (tallyApiLimiter as any).lastRefill = Date.now() - 2000; // 2 seconds ago app.use(tallyApiRateLimit); app.get('/tally', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/tally') .expect(200); expect(response.body).toEqual({ success: true }); }); }); describe('Composite Rate Limiter', () => { it('should apply both rate limiters', async () => { const compositeRateLimiters = createCompositeRateLimiter({ windowMs: 60000, max: 5, message: { error: 'Too many requests', retryAfter: '1 minute' }, standardHeaders: true, legacyHeaders: false }); app.use('/composite', ...compositeRateLimiters); app.get('/composite/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/composite/test') .expect(200); expect(response.body).toEqual({ success: true }); const limitHeader = response.headers['x-ratelimit-limit'] || response.headers['ratelimit-limit']; expect(limitHeader).toBe('5'); expect(response.headers['x-tally-ratelimit-limit']).toBe('100'); }); it('should be blocked by Tally API limiter when tokens exhausted', async () => { // Exhaust Tally API tokens (tallyApiLimiter as any).tokens = 0; const compositeRateLimiters = createCompositeRateLimiter({ windowMs: 60000, max: 100, message: { error: 'Too many requests', retryAfter: '1 minute' }, standardHeaders: true, legacyHeaders: false }); app.use('/composite', ...compositeRateLimiters); app.get('/composite/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/composite/test') .expect(429); expect(response.body.error).toBe('Tally API rate limit exceeded'); }); }); describe('Rate Limit Error Handler', () => { it('should handle rate limit errors', async () => { app.use((req, res, next) => { const error = new Error('Rate limit exceeded'); (error as any).status = 429; (error as any).retryAfter = 600; next(error); }); app.use(rateLimitErrorHandler); app.get('/test', (req, res) => { res.json({ success: true }); }); const response = await request(app) .get('/test') .expect(429); expect(response.body.error).toBe('Rate limit exceeded'); expect(response.body.retryAfter).toBe(600); expect(response.body.path).toBe('/test'); expect(response.body.timestamp).toBeDefined(); }); it('should pass through non-rate-limit errors', async () => { const mockNext = jest.fn(); app.use((req, res, next) => { const error = new Error('Some other error'); (error as any).status = 500; next(error); }); app.use(rateLimitErrorHandler); // Since this would call next() with the error, we need to test the function directly const mockError = new Error('Some other error'); (mockError as any).status = 500; const mockReq = {} as any; const mockRes = {} as any; rateLimitErrorHandler(mockError, mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalledWith(mockError); }); }); describe('Rate Limit Status Utility', () => { it('should return current rate limit status', () => { const mockReq = { ip: '127.0.0.1', path: '/test', get: jest.fn((header: string) => { const headers: Record<string, string> = { 'X-RateLimit-Limit': '100', 'X-RateLimit-Remaining': '50', 'X-RateLimit-Reset': '1234567890' }; return headers[header]; }) } as any; const status = getRateLimitStatus(mockReq); expect(status).toEqual({ ip: '127.0.0.1', path: '/test', tallyApiTokensRemaining: expect.any(Number), tallyApiTimeUntilNextToken: expect.any(Number), headers: { rateLimit: '100', rateLimitRemaining: '50', rateLimitReset: '1234567890' } }); }); }); // Note: Redis integration tests removed as Redis store is not currently implemented // Tests now focus on memory store functionality describe('Token Bucket Algorithm', () => { it('should refill tokens at the correct rate', () => { const limiter = (tallyApiLimiter as any); // Set up initial state limiter.tokens = 50; limiter.lastRefill = Date.now() - 5000; // 5 seconds ago // Trigger refill by checking if we can make a request const canMake = limiter.canMakeRequest(); expect(canMake).toBe(true); expect(limiter.tokens).toBeGreaterThan(50); // Should have refilled some tokens }); it('should not exceed maximum tokens', () => { const limiter = (tallyApiLimiter as any); // Set up state where we would refill more than max limiter.tokens = 99; limiter.lastRefill = Date.now() - 10000; // 10 seconds ago (would add 10 tokens) limiter.refillTokens(); expect(limiter.tokens).toBe(100); // Should cap at maxTokens }); it('should calculate correct time until next token', () => { const limiter = (tallyApiLimiter as any); // Empty bucket limiter.tokens = 0; limiter.lastRefill = Date.now(); const timeUntilNext = limiter.getTimeUntilNextToken(); expect(timeUntilNext).toBe(1000); // 1 second with 1 token per second rate }); }); }); describe('Rate Limit Configurations', () => { it('should have correct strict configuration', () => { expect(rateLimitConfigs.strict).toEqual({ windowMs: 15 * 60 * 1000, max: 10, message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }); }); it('should have correct standard configuration', () => { expect(rateLimitConfigs.standard).toEqual({ windowMs: 15 * 60 * 1000, max: 100, message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }); }); it('should have correct lenient configuration', () => { expect(rateLimitConfigs.lenient).toEqual({ windowMs: 15 * 60 * 1000, max: 1000, message: { error: 'Too many requests, please try again later.', retryAfter: '15 minutes' }, standardHeaders: true, legacyHeaders: false }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server