Skip to main content
Glama

Open Food Facts MCP Server

by caleb-conner
rate-limiting.test.ts14.9 kB
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { OpenFoodFactsClient } from '../src/client.js'; import { OpenFoodFactsConfig } from '../src/types.js'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { mockNutellaProductResponse, mockSearchResponse } from './fixtures/products.js'; describe('Rate Limiting', () => { let client: OpenFoodFactsClient; let mockAxios: MockAdapter; let config: OpenFoodFactsConfig; beforeEach(() => { jest.useFakeTimers(); config = { baseUrl: 'https://test.openfoodfacts.net', userAgent: 'OpenFoodFactsMCP/1.0 (test)', rateLimits: { products: 3, // Strict limits for testing search: 2, // Strict limits for testing facets: 1, // Strict limits for testing }, }; client = new OpenFoodFactsClient(config); mockAxios = new MockAdapter(axios); // Mock successful responses for all endpoints mockAxios.onGet(/\/api\/v2\/product/).reply(200, mockNutellaProductResponse); mockAxios.onGet(/\/cgi\/search\.pl/).reply(200, mockSearchResponse); }); afterEach(() => { mockAxios.restore(); jest.useRealTimers(); jest.clearAllMocks(); }); describe('Product Rate Limiting', () => { it('should allow requests within rate limit', async () => { // Should allow up to 3 requests await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // All should succeed without throwing expect(mockAxios.history.get).toHaveLength(3); }); it('should enforce rate limit after exceeding threshold', async () => { // Use up the rate limit await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // Fourth request should fail await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // Verify only 3 requests were made expect(mockAxios.history.get).toHaveLength(3); }); it('should reset rate limit after time window', async () => { // Exhaust rate limit await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // Should fail await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // Advance time by 61 seconds (past the 60-second window) jest.advanceTimersByTime(61000); // Should now succeed await client.getProduct('123456793'); expect(mockAxios.history.get).toHaveLength(4); }); it('should provide accurate wait time in error message', async () => { const startTime = Date.now(); // Exhaust rate limit await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // Advance time by 30 seconds jest.advanceTimersByTime(30000); try { await client.getProduct('123456792'); fail('Should have thrown rate limit error'); } catch (error) { expect(error).toBeInstanceOf(Error); const errorMessage = (error as Error).message; expect(errorMessage).toContain('Rate limit exceeded for products'); expect(errorMessage).toContain('Wait 30 seconds'); } }); it('should handle partial rate limit reset correctly', async () => { // Make 2 requests await client.getProduct('123456789'); await client.getProduct('123456790'); // Advance time by 30 seconds (half the window) jest.advanceTimersByTime(30000); // Make 1 more request (should succeed, total = 3) await client.getProduct('123456791'); // Next request should fail (would be 4th) await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // Advance time by another 31 seconds (past original window) jest.advanceTimersByTime(31000); // Should now succeed (new window) await client.getProduct('123456793'); }); it('should track requests accurately with concurrent calls', async () => { // Start multiple requests simultaneously const promises = [ client.getProduct('123456789'), client.getProduct('123456790'), client.getProduct('123456791'), ]; await Promise.all(promises); // Fourth request should fail await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); }); }); describe('Search Rate Limiting', () => { it('should allow requests within search rate limit', async () => { await client.searchProducts({ search: 'test1' }); await client.searchProducts({ search: 'test2' }); expect(mockAxios.history.get.filter(req => req.url?.includes('/cgi/search.pl'))).toHaveLength(2); }); it('should enforce search rate limit', async () => { // Use up search rate limit (2 requests) await client.searchProducts({ search: 'test1' }); await client.searchProducts({ search: 'test2' }); // Third request should fail await expect(client.searchProducts({ search: 'test3' })) .rejects.toThrow('Rate limit exceeded for search'); }); it('should reset search rate limit after time window', async () => { // Exhaust search rate limit await client.searchProducts({ search: 'test1' }); await client.searchProducts({ search: 'test2' }); // Should fail await expect(client.searchProducts({ search: 'test3' })) .rejects.toThrow('Rate limit exceeded for search'); // Advance time past window jest.advanceTimersByTime(61000); // Should now succeed await client.searchProducts({ search: 'test4' }); }); }); describe('Independent Rate Limit Tracking', () => { it('should track different endpoints independently', async () => { // Use up product rate limit await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // Products should be blocked await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // But search should still work (independent limit) await client.searchProducts({ search: 'test' }); await client.searchProducts({ search: 'test2' }); // Now search should be blocked too await expect(client.searchProducts({ search: 'test3' })) .rejects.toThrow('Rate limit exceeded for search'); // Verify correct number of requests per endpoint const productRequests = mockAxios.history.get.filter(req => req.url?.includes('/api/v2/product')); const searchRequests = mockAxios.history.get.filter(req => req.url?.includes('/cgi/search.pl')); expect(productRequests).toHaveLength(3); expect(searchRequests).toHaveLength(2); }); it('should reset different endpoints independently', async () => { // Exhaust both limits at different times await client.getProduct('123456789'); // Advance time by 30 seconds jest.advanceTimersByTime(30000); await client.searchProducts({ search: 'test1' }); await client.getProduct('123456790'); await client.getProduct('123456791'); await client.searchProducts({ search: 'test2' }); // Both should be blocked await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); await expect(client.searchProducts({ search: 'test3' })) .rejects.toThrow('Rate limit exceeded for search'); // Advance time by 31 more seconds (61 total from first product request) jest.advanceTimersByTime(31000); // Products should be unblocked (past 60s from first request) await client.getProduct('123456793'); // But search should still be blocked (only 31s since first search) await expect(client.searchProducts({ search: 'test4' })) .rejects.toThrow('Rate limit exceeded for search'); // Advance time by 30 more seconds (61s from first search) jest.advanceTimersByTime(30000); // Now search should also be unblocked await client.searchProducts({ search: 'test5' }); }); }); describe('Rate Limit Configuration', () => { it('should respect custom rate limits', async () => { // Create client with very strict limits const strictConfig = { ...config, rateLimits: { products: 1, search: 1, facets: 1 }, }; const strictClient = new OpenFoodFactsClient(strictConfig); // First request should succeed await strictClient.getProduct('123456789'); // Second request should immediately fail await expect(strictClient.getProduct('123456790')) .rejects.toThrow('Rate limit exceeded for products'); }); it('should handle zero rate limits', async () => { const zeroConfig = { ...config, rateLimits: { products: 0, search: 0, facets: 0 }, }; const zeroClient = new OpenFoodFactsClient(zeroConfig); // First request should fail immediately await expect(zeroClient.getProduct('123456789')) .rejects.toThrow('Rate limit exceeded for products'); }); it('should handle very high rate limits', async () => { const highConfig = { ...config, rateLimits: { products: 1000, search: 1000, facets: 1000 }, }; const highClient = new OpenFoodFactsClient(highConfig); // Should allow many requests for (let i = 0; i < 10; i++) { await highClient.getProduct(`12345678${i}`); } expect(mockAxios.history.get).toHaveLength(10); }); }); describe('Edge Cases and Error Conditions', () => { it('should handle system time changes gracefully', async () => { // Exhaust rate limit await client.getProduct('123456789'); await client.getProduct('123456790'); await client.getProduct('123456791'); // Mock system time going backwards (shouldn't happen, but test resilience) const mockDateNow = jest.spyOn(Date, 'now'); mockDateNow.mockReturnValue(Date.now() - 120000); // 2 minutes ago // Should still respect rate limit (use max of current and stored time) await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); mockDateNow.mockRestore(); }); it('should handle concurrent rate limit checks correctly', async () => { // Start requests simultaneously that would exceed limit const promises = []; for (let i = 0; i < 5; i++) { promises.push(client.getProduct(`12345678${i}`).catch(error => error)); } const results = await Promise.all(promises); // Should have 3 successes and 2 failures const successes = results.filter(result => !(result instanceof Error)); const failures = results.filter(result => result instanceof Error); expect(successes).toHaveLength(3); expect(failures).toHaveLength(2); failures.forEach(error => { expect(error.message).toContain('Rate limit exceeded'); }); }); it('should maintain rate limit state across different request types', async () => { // Mix different types of requests await client.getProduct('123456789'); await client.searchProducts({ search: 'test1' }); await client.getProduct('123456790'); await client.searchProducts({ search: 'test2' }); await client.getProduct('123456791'); // Next product request should fail await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // Next search request should fail await expect(client.searchProducts({ search: 'test3' })) .rejects.toThrow('Rate limit exceeded for search'); }); it('should handle rate limit errors without affecting successful requests', async () => { // Make successful requests await client.getProduct('123456789'); await client.getProduct('123456790'); // This should succeed const successfulResponse = await client.getProduct('123456791'); expect(successfulResponse).toEqual(mockNutellaProductResponse); // This should fail but not affect future valid requests await expect(client.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // After time passes, requests should work again jest.advanceTimersByTime(61000); const futureResponse = await client.getProduct('123456793'); expect(futureResponse).toEqual(mockNutellaProductResponse); }); }); describe('Memory Management', () => { it('should clean up old rate limit trackers', async () => { // Create many different endpoint calls to simulate memory usage const client1 = new OpenFoodFactsClient({ ...config, rateLimits: { products: 1000, search: 1000, facets: 1000 }, }); // Make requests for different "endpoints" (in real implementation, this would be different internal tracking) for (let i = 0; i < 100; i++) { await client1.getProduct(`12345678${i % 10}`); } // Advance time significantly jest.advanceTimersByTime(300000); // 5 minutes // Should still work without memory issues await client1.getProduct('999999999'); expect(mockAxios.history.get.length).toBeGreaterThan(100); }); it('should handle multiple clients independently', async () => { const client1 = new OpenFoodFactsClient(config); const client2 = new OpenFoodFactsClient(config); // Exhaust rate limit for client1 await client1.getProduct('123456789'); await client1.getProduct('123456790'); await client1.getProduct('123456791'); // client1 should be blocked await expect(client1.getProduct('123456792')) .rejects.toThrow('Rate limit exceeded for products'); // But client2 should still work (independent state) await client2.getProduct('123456793'); await client2.getProduct('123456794'); await client2.getProduct('123456795'); // Now client2 should also be blocked await expect(client2.getProduct('123456796')) .rejects.toThrow('Rate limit exceeded for products'); }); }); });

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/caleb-conner/open-food-facts-mcp'

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