Skip to main content
Glama
rate-limiting.integration.test.ts13.8 kB
import { describe, it, expect } from '@jest/globals'; import { executeTool, executeBatch, sleep } from './utils/test-helpers.ts'; import { TEST_CONFIG } from './setup.ts'; describe('Rate Limiting Integration Tests', () => { // Skip rate limiting tests if not using real API const skipIfMocked = (): boolean => { if (!TEST_CONFIG.enableRealApiCalls) { console.warn('Skipping rate limiting test - real API calls disabled'); return true; } return false; }; describe('Basic Rate Limiting', () => { it('should respect rate limits', async () => { if (skipIfMocked()) return; const requests = Array.from( { length: 10 }, () => (): Promise<unknown> => executeTool('list-projects', { 'per-page': 1 }) ); const start = Date.now(); const results = await executeBatch(requests, 5); const duration = Date.now() - start; expect(results).toHaveLength(10); // For real API tests, we're mainly testing that requests complete successfully // Rate limiting behavior varies by API implementation if (TEST_CONFIG.enableRealApiCalls) { console.log(`Rate limiting test completed in ${duration}ms`); // Real APIs may be fast and not hit rate limits with small request counts expect(duration).toBeGreaterThan(0); } else { expect(duration).toBeGreaterThan(500); // Mock API should simulate rate limiting } // All requests should succeed results.forEach((result) => { expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); }); }); it('should handle burst requests gracefully', async () => { if (skipIfMocked()) return; // Make many requests simultaneously const requests = Array.from({ length: 20 }, () => executeTool('list-projects', { 'per-page': 1 }) ); const start = Date.now(); const results = await Promise.allSettled(requests); const duration = Date.now() - start; expect(results).toHaveLength(20); // For real API tests, timing expectations are more flexible if (TEST_CONFIG.enableRealApiCalls) { console.log(`Burst request test completed in ${duration}ms`); expect(duration).toBeGreaterThan(0); } else { expect(duration).toBeGreaterThan(1000); // Mock API should simulate rate limiting } // Most requests should succeed, some might fail with rate limit errors const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; expect(successful).toBeGreaterThan(0); if (failed > 0) { console.log(`${failed} requests failed due to rate limiting (expected)`); } }); }); describe('Rate Limit Error Handling', () => { it('should handle 429 rate limit errors', async () => { if (skipIfMocked()) return; // Create a large number of requests to trigger rate limiting const requests = Array.from({ length: 100 }, () => executeTool('list-projects', { 'per-page': 1 }) ); try { await Promise.all(requests); console.warn('Rate limit not reached - may need to adjust test parameters'); } catch (error) { expect(error).toBeDefined(); expect(error instanceof Error).toBe(true); const errorMessage = (error as Error).message.toLowerCase(); expect( errorMessage.includes('rate limit') || errorMessage.includes('429') || errorMessage.includes('too many requests') ).toBe(true); } }); it('should provide retry information in rate limit errors', async () => { if (skipIfMocked()) return; const requests = Array.from({ length: 50 }, () => executeTool('list-projects', { 'per-page': 1 }) ); try { await Promise.all(requests); console.warn('Rate limit not reached - may need to adjust test parameters'); } catch (error) { expect(error).toBeDefined(); // Check if error contains retry information const errorMessage = (error as Error).message; if (errorMessage.includes('rate limit') || errorMessage.includes('429')) { // Rate limit error should contain helpful information expect(errorMessage.length).toBeGreaterThan(10); } } }); }); describe('Rate Limit Recovery', () => { it('should recover after rate limit period', async () => { if (skipIfMocked()) return; if (TEST_CONFIG.skipSlowTests) { console.warn('Skipping rate limit recovery test - slow tests disabled'); return; } // Make rapid requests to trigger rate limiting const promises = []; for (let i = 0; i < 10; i++) { promises.push(executeTool('list-projects', { per_page: 1 })); } try { await Promise.all(promises); } catch (error) { // Expected to hit rate limit console.log('Rate limit triggered as expected:', error.message); } // Wait for rate limit to reset (Float API typically resets every minute) console.log('Waiting for rate limit to reset...'); await sleep(65000); // Wait 65 seconds to be safe // Should be able to make requests again const result = await executeTool('list-projects', { per_page: 1 }); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); }, 120000); // 2 minute timeout it('should handle gradual request increase', async () => { if (skipIfMocked()) return; if (TEST_CONFIG.skipSlowTests) { console.warn('Skipping gradual request test - slow tests disabled'); return; } const results = []; let consecutiveSuccesses = 0; // Gradually increase request frequency for (let i = 1; i <= 5; i++) { try { const result = await executeTool('list-projects', { per_page: 1 }); results.push(result); consecutiveSuccesses++; // Wait progressively shorter intervals await sleep(Math.max(1000, 5000 - i * 800)); } catch (error) { console.log(`Request ${i} failed (expected):`, error.message); // Reset counter and wait longer consecutiveSuccesses = 0; await sleep(10000); } } // Should have some successful requests expect(consecutiveSuccesses).toBeGreaterThan(0); expect(results.length).toBeGreaterThan(0); }, 120000); // 2 minute timeout }); describe('Rate Limit Metrics', () => { it('should track request timing', async () => { if (skipIfMocked()) return; const timings: number[] = []; for (let i = 0; i < 5; i++) { const start = Date.now(); await executeTool('list-projects', { 'per-page': 1 }); const duration = Date.now() - start; timings.push(duration); // Add small delay between requests await sleep(TEST_CONFIG.apiCallDelay); } expect(timings).toHaveLength(5); // Log timing statistics const avgTiming = timings.reduce((a, b) => a + b) / timings.length; const maxTiming = Math.max(...timings); const minTiming = Math.min(...timings); console.log( `Request timings - Avg: ${avgTiming}ms, Max: ${maxTiming}ms, Min: ${minTiming}ms` ); // All requests should complete within reasonable time timings.forEach((timing) => { expect(timing).toBeLessThan(30000); // 30 seconds max }); }); it('should measure concurrent request performance', async () => { if (skipIfMocked()) return; const concurrentRequests = 5; const requests = Array.from({ length: concurrentRequests }, () => executeTool('list-projects', { 'per-page': 1 }) ); const start = Date.now(); const results = await Promise.all(requests); const duration = Date.now() - start; expect(results).toHaveLength(concurrentRequests); // Calculate average time per request const avgTimePerRequest = duration / concurrentRequests; console.log( `Concurrent requests - Total: ${duration}ms, Avg per request: ${avgTimePerRequest}ms` ); // Concurrent requests should be more efficient than sequential expect(avgTimePerRequest).toBeLessThan(duration); }); }); describe('Tool-Specific Rate Limiting', () => { const toolsToTest = [ 'list-projects', 'list-people', 'list-tasks', 'list-allocations', 'list-clients', ]; toolsToTest.forEach((toolName) => { it(`should handle rate limiting for ${toolName}`, async () => { if (skipIfMocked()) return; const requests = Array.from({ length: 10 }, () => executeTool(toolName, { 'per-page': 1 })); const start = Date.now(); const results = await Promise.allSettled(requests); const duration = Date.now() - start; expect(results).toHaveLength(10); // For real API tests, timing expectations are more flexible if (TEST_CONFIG.enableRealApiCalls) { console.log(`${toolName} rate limiting test completed in ${duration}ms`); expect(duration).toBeGreaterThan(0); } else { expect(duration).toBeGreaterThan(500); // Mock API should simulate rate limiting } const successful = results.filter((r) => r.status === 'fulfilled').length; const failed = results.filter((r) => r.status === 'rejected').length; expect(successful).toBeGreaterThan(0); if (failed > 0) { console.log(`${toolName}: ${failed} requests failed due to rate limiting`); } }); }); }); describe('Rate Limit Configuration', () => { it('should respect configured rate limits', async () => { if (skipIfMocked()) return; // Check current rate limit configuration const rateLimitWindow = parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'); const rateLimitMax = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'); expect(rateLimitWindow).toBeGreaterThan(0); expect(rateLimitMax).toBeGreaterThan(0); console.log( `Rate limit config - Window: ${rateLimitWindow}ms, Max: ${rateLimitMax} requests` ); // Test with configuration values const testRequests = Math.min(rateLimitMax / 2, 20); // Don't exceed half the limit const requests = Array.from({ length: testRequests }, () => executeTool('list-projects', { 'per-page': 1 }) ); const start = Date.now(); const results = await Promise.allSettled(requests); const duration = Date.now() - start; expect(results).toHaveLength(testRequests); const successful = results.filter((r) => r.status === 'fulfilled').length; expect(successful).toBeGreaterThan(0); console.log( `Config test - ${successful}/${testRequests} requests succeeded in ${duration}ms` ); }); }); describe('Error Response Validation', () => { it('should validate rate limit error response format', async () => { if (skipIfMocked()) return; const requests = Array.from({ length: 100 }, () => executeTool('list-projects', { 'per-page': 1 }) ); try { await Promise.all(requests); console.warn('Rate limit not reached - cannot test error format'); } catch (error) { expect(error).toBeDefined(); expect(error instanceof Error).toBe(true); // Validate error structure const errorMessage = (error as Error).message; expect(errorMessage).toBeDefined(); expect(errorMessage.length).toBeGreaterThan(0); // Check for expected error indicators const lowerMessage = errorMessage.toLowerCase(); const hasRateLimitIndicator = lowerMessage.includes('rate limit') || lowerMessage.includes('429') || lowerMessage.includes('too many requests') || lowerMessage.includes('throttle'); expect(hasRateLimitIndicator).toBe(true); } }); }); describe('Performance Impact', () => { it('should measure rate limiting impact on performance', async () => { if (skipIfMocked()) return; if (TEST_CONFIG.skipSlowTests) { console.warn('Skipping performance impact test - slow tests disabled'); return; } // Measure performance with deliberate pacing const pacedRequests = async (): Promise<number> => { const start = Date.now(); for (let i = 0; i < 5; i++) { await executeTool('list-projects', { 'per-page': 1 }); await sleep(1000); // 1 second between requests } return Date.now() - start; }; // Measure performance without pacing (will likely hit rate limits) const rapidRequests = async (): Promise<number> => { const start = Date.now(); const requests = Array.from({ length: 5 }, () => executeTool('list-projects', { 'per-page': 1 }) ); try { await Promise.all(requests); } catch (error) { // Some requests may fail due to rate limiting } return Date.now() - start; }; const [pacedTime, rapidTime] = await Promise.all([pacedRequests(), rapidRequests()]); console.log(`Performance comparison - Paced: ${pacedTime}ms, Rapid: ${rapidTime}ms`); // Paced requests should be more predictable expect(pacedTime).toBeGreaterThan(4000); // At least 4 seconds with 1s delays expect(rapidTime).toBeLessThan(pacedTime); // Rapid should be faster overall }); }); });

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/asachs01/float-mcp'

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