Skip to main content
Glama

Open Food Facts MCP Server

by caleb-conner
mock-server.ts8.51 kB
import nock from 'nock'; import { ProductResponse, SearchResponse } from '../../src/types.js'; import { mockNutellaProductResponse, mockHealthyYogurtProductResponse, mockVeganProductResponse, mockProductNotFoundResponse, mockSearchResponse, mockEmptySearchResponse } from '../fixtures/products.js'; /** * Mock server utilities for testing HTTP interactions */ export class MockOpenFoodFactsServer { private scope: nock.Scope; private baseUrl: string; constructor(baseUrl = 'https://test.openfoodfacts.net') { this.baseUrl = baseUrl; this.scope = nock(baseUrl); } /** * Mock a successful product lookup */ mockGetProduct(barcode: string, response: ProductResponse = mockNutellaProductResponse): this { this.scope .get(`/api/v2/product/${barcode}`) .reply(200, response); return this; } /** * Mock a product not found */ mockGetProductNotFound(barcode: string): this { this.scope .get(`/api/v2/product/${barcode}`) .reply(200, mockProductNotFoundResponse); return this; } /** * Mock a product lookup with error */ mockGetProductError(barcode: string, status = 500, message = 'Server Error'): this { this.scope .get(`/api/v2/product/${barcode}`) .reply(status, message); return this; } /** * Mock a network error for product lookup */ mockGetProductNetworkError(barcode: string): this { this.scope .get(`/api/v2/product/${barcode}`) .replyWithError('Network Error'); return this; } /** * Mock a successful search */ mockSearch(params: Record<string, any> = {}, response: SearchResponse = mockSearchResponse): this { let path = '/cgi/search.pl'; if (Object.keys(params).length > 0) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { searchParams.append(key, String(value)); }); path += `?${searchParams.toString()}`; } this.scope .get(path) .reply(200, response); return this; } /** * Mock search with any parameters */ mockSearchAny(response: SearchResponse = mockSearchResponse): this { this.scope .get('/cgi/search.pl') .query(true) // Match any query parameters .reply(200, response); return this; } /** * Mock an empty search result */ mockSearchEmpty(params: Record<string, any> = {}): this { return this.mockSearch(params, mockEmptySearchResponse); } /** * Mock a search error */ mockSearchError(status = 500, message = 'Server Error'): this { this.scope .get('/cgi/search.pl') .query(true) .reply(status, message); return this; } /** * Mock rate limit error */ mockRateLimit(endpoint: 'product' | 'search'): this { const path = endpoint === 'product' ? '/api/v2/product' : '/cgi/search.pl'; this.scope .get(new RegExp(path)) .reply(429, 'Too Many Requests'); return this; } /** * Mock multiple products for comparison testing */ mockMultipleProducts(barcodes: string[], responses?: ProductResponse[]): this { barcodes.forEach((barcode, index) => { const response = responses?.[index] || this.getDefaultResponse(index); this.mockGetProduct(barcode, response); }); return this; } /** * Mock search with pagination */ mockPaginatedSearch( totalCount: number, pageSize: number, currentPage: number, products: any[] = [] ): this { const pageCount = Math.ceil(totalCount / pageSize); const response: SearchResponse = { count: totalCount, page: currentPage, page_count: pageCount, page_size: pageSize, products, }; this.scope .get('/cgi/search.pl') .query(query => query.page === String(currentPage)) .reply(200, response); return this; } /** * Mock delayed response for timeout testing */ mockDelayedResponse(barcode: string, delayMs: number, response: ProductResponse = mockNutellaProductResponse): this { this.scope .get(`/api/v2/product/${barcode}`) .delay(delayMs) .reply(200, response); return this; } /** * Mock intermittent failures */ mockIntermittentFailure(barcode: string, failCount: number): this { // First few calls fail for (let i = 0; i < failCount; i++) { this.scope .get(`/api/v2/product/${barcode}`) .reply(500, 'Temporary Server Error'); } // Then succeed this.scope .get(`/api/v2/product/${barcode}`) .reply(200, mockNutellaProductResponse); return this; } /** * Verify that all mocked endpoints were called */ isDone(): boolean { return this.scope.isDone(); } /** * Get pending mocks (not yet called) */ pendingMocks(): string[] { return this.scope.pendingMocks(); } /** * Clean up all mocks */ cleanup(): void { nock.cleanAll(); } /** * Get request history for debugging */ getRequestHistory(): any[] { return (nock as any).recorder.records() || []; } /** * Mock the entire OFF API server with common endpoints */ mockCompleteServer(): this { // Mock popular products this.mockGetProduct('3017620422003', mockNutellaProductResponse); this.mockGetProduct('3229820787015', mockHealthyYogurtProductResponse); this.mockGetProduct('8712566073219', mockVeganProductResponse); // Mock search endpoints this.mockSearchAny(mockSearchResponse); // Mock 404s for unknown products this.scope .get(/\/api\/v2\/product\/0+/) .reply(200, mockProductNotFoundResponse); return this; } /** * Mock server with realistic delays */ mockRealisticServer(): this { // Product lookups - fast this.scope .get(/\/api\/v2\/product\/\d+/) .delay(50) .reply(200, mockNutellaProductResponse); // Search - slower this.scope .get('/cgi/search.pl') .query(true) .delay(200) .reply(200, mockSearchResponse); return this; } /** * Mock server that respects rate limits */ mockRateLimitedServer(requestsPerMinute: number): this { let requestCount = 0; const resetTime = Date.now() + 60000; // 1 minute from now this.scope .get(/\/api\/v2\/product\/\d+/) .reply(() => { if (Date.now() > resetTime) { requestCount = 0; // Reset counter } requestCount++; if (requestCount > requestsPerMinute) { return [429, 'Rate limit exceeded']; } return [200, mockNutellaProductResponse]; }) .persist(); return this; } private getDefaultResponse(index: number): ProductResponse { const responses = [ mockNutellaProductResponse, mockHealthyYogurtProductResponse, mockVeganProductResponse, ]; return responses[index % responses.length]; } } /** * Helper function to create a mock server */ export function createMockServer(baseUrl?: string): MockOpenFoodFactsServer { return new MockOpenFoodFactsServer(baseUrl); } /** * Helper to mock a complete test scenario */ export function mockTestScenario(scenario: 'success' | 'errors' | 'mixed' | 'rate-limited'): MockOpenFoodFactsServer { const server = createMockServer(); switch (scenario) { case 'success': return server.mockCompleteServer(); case 'errors': server.mockGetProductError('3017620422003', 500); server.mockSearchError(500); return server; case 'mixed': server.mockGetProduct('3017620422003', mockNutellaProductResponse); server.mockGetProductNotFound('0000000000000'); server.mockGetProductError('1111111111111', 500); server.mockSearchAny(mockSearchResponse); return server; case 'rate-limited': return server.mockRateLimitedServer(5); default: return server; } } /** * Utility to wait for all HTTP mocks to complete */ export async function waitForMocks(server: MockOpenFoodFactsServer, timeoutMs = 5000): Promise<void> { const startTime = Date.now(); while (!server.isDone() && (Date.now() - startTime) < timeoutMs) { await new Promise(resolve => setTimeout(resolve, 10)); } if (!server.isDone()) { throw new Error(`Mocks not completed within ${timeoutMs}ms. Pending: ${server.pendingMocks().join(', ')}`); } }

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