Skip to main content
Glama

Open Food Facts MCP Server

by caleb-conner
handlers.test.ts20 kB
import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { ToolHandlers } from '../src/handlers.js'; import { OpenFoodFactsClient } from '../src/client.js'; import { mockNutellaProduct, mockHealthyYogurtProduct, mockVeganProduct, mockNutellaProductResponse, mockHealthyYogurtProductResponse, mockVeganProductResponse, mockProductNotFoundResponse, mockSearchResponse, mockEmptySearchResponse, mockBarcodes, } from './fixtures/products.js'; // Mock the client jest.mock('../src/client.js'); describe('ToolHandlers', () => { let handlers: ToolHandlers; let mockClient: jest.Mocked<OpenFoodFactsClient>; beforeEach(() => { mockClient = { getProduct: jest.fn(), searchProducts: jest.fn(), getProductsByBarcodes: jest.fn(), } as any; handlers = new ToolHandlers(mockClient); }); describe('handleGetProduct', () => { it('should format product successfully', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleGetProduct(mockBarcodes.nutella); expect(mockClient.getProduct).toHaveBeenCalledWith(mockBarcodes.nutella); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); const text = result.content[0].text; expect(text).toContain('**Nutella**'); expect(text).toContain('Brand: Ferrero'); expect(text).toContain('Nutri-Score: E'); expect(text).toContain('NOVA Group: 4'); expect(text).toContain('Eco-Score: D'); expect(text).toContain('Energy: 539 kcal'); expect(text).toContain('Fat: 30.9g'); expect(text).toContain('Sugar, palm oil, hazelnuts'); }); it('should handle product not found', async () => { mockClient.getProduct.mockResolvedValue(mockProductNotFoundResponse); const result = await handlers.handleGetProduct(mockBarcodes.notFound); expect(result.content[0].text).toContain('Product not found for barcode'); expect(result.content[0].text).toContain(mockBarcodes.notFound); }); it('should handle product with minimal data', async () => { const minimalResponse = { status: 1, status_verbose: 'product found', product: { code: '123456789', product_name: 'Test Product', }, }; mockClient.getProduct.mockResolvedValue(minimalResponse as any); const result = await handlers.handleGetProduct('123456789'); expect(result.content[0].text).toContain('**Test Product**'); expect(result.content[0].text).not.toContain('**Scores:**'); expect(result.content[0].text).not.toContain('**Nutrition**'); }); it('should handle missing product name', async () => { const responseWithoutName = { ...mockNutellaProductResponse, product: { ...mockNutellaProduct, product_name: undefined, }, }; mockClient.getProduct.mockResolvedValue(responseWithoutName as any); const result = await handlers.handleGetProduct(mockBarcodes.nutella); expect(result.content[0].text).toContain('**Unknown Product**'); }); it('should format all nutritional information correctly', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleGetProduct(mockBarcodes.nutella); const text = result.content[0].text; // Check all key nutrients are present expect(text).toContain('Energy: 539 kcal'); expect(text).toContain('Fat: 30.9g'); expect(text).toContain('Carbs: 57.5g'); expect(text).toContain('Sugars: 56.3g'); expect(text).toContain('Protein: 6.3g'); expect(text).toContain('Salt: 0.107g'); expect(text).not.toContain('Fiber: 0g'); // Should not show zero fiber }); it('should format additional info correctly', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleGetProduct(mockBarcodes.nutella); const text = result.content[0].text; expect(text).toContain('**Additional Info:**'); expect(text).toContain('Packaging: Plastic jar'); expect(text).toContain('Labels: No palm oil'); expect(text).toContain('Countries: France'); }); }); describe('handleSearchProducts', () => { it('should format search results successfully', async () => { mockClient.searchProducts.mockResolvedValue(mockSearchResponse); const result = await handlers.handleSearchProducts({ search: 'chocolate' }); expect(mockClient.searchProducts).toHaveBeenCalledWith({ search: 'chocolate' }); expect(result.content[0].text).toContain('Found 150 products'); expect(result.content[0].text).toContain('showing page 1 of 8'); expect(result.content[0].text).toContain('1. **Nutella**'); expect(result.content[0].text).toContain('[Nutri-Score: E]'); expect(result.content[0].text).toContain('Barcode: 3017620422003'); }); it('should handle empty search results', async () => { mockClient.searchProducts.mockResolvedValue(mockEmptySearchResponse); const result = await handlers.handleSearchProducts({ search: 'nonexistent' }); expect(result.content[0].text).toBe('No products found matching your search criteria.'); }); it('should format product summaries correctly', async () => { mockClient.searchProducts.mockResolvedValue(mockSearchResponse); const result = await handlers.handleSearchProducts({}); const text = result.content[0].text; // Check all three products are formatted expect(text).toContain('**Nutella** (Ferrero) [Nutri-Score: E]'); expect(text).toContain('**Greek Yogurt Natural** (Danone) [Nutri-Score: B]'); expect(text).toContain('**Oat Drink Original** (Alpro) [Nutri-Score: B]'); }); it('should handle products without brands or nutriscore', async () => { const searchWithMinimalData = { ...mockSearchResponse, products: [{ code: '123456789', product_name: 'Test Product', }], }; mockClient.searchProducts.mockResolvedValue(searchWithMinimalData as any); const result = await handlers.handleSearchProducts({}); const text = result.content[0].text; expect(text).toContain('**Test Product**'); expect(text).not.toContain('('); // No brand parentheses expect(text).not.toContain('[Nutri-Score:'); // No nutriscore }); }); describe('handleAnalyzeProduct', () => { it('should provide comprehensive nutritional analysis', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleAnalyzeProduct(mockBarcodes.nutella); const text = result.content[0].text; expect(text).toContain('**Nutritional Analysis: Nutella**'); expect(text).toContain('• Nutri-Score E: Very poor nutritional quality'); expect(text).toContain('• NOVA Group 4: Ultra-processed foods'); expect(text).toContain('• Eco-Score D: High environmental impact'); expect(text).toContain('**Nutritional Breakdown:**'); expect(text).toContain('• Energy: 539 kcal (high)'); expect(text).toContain('• Fat: 30.9g (high)'); expect(text).toContain('• Sugars: 56.3g (high)'); expect(text).toContain('• Salt: 0.107g (low)'); }); it('should handle healthy product analysis', async () => { mockClient.getProduct.mockResolvedValue(mockHealthyYogurtProductResponse); const result = await handlers.handleAnalyzeProduct(mockBarcodes.yogurt); const text = result.content[0].text; expect(text).toContain('• Nutri-Score B: Good nutritional quality'); expect(text).toContain('• NOVA Group 1: Unprocessed or minimally processed foods'); expect(text).toContain('• Energy: 97 kcal (low)'); expect(text).toContain('• Fat: 5g (moderate)'); expect(text).toContain('• Sugars: 4g (low)'); }); it('should handle product not found for analysis', async () => { mockClient.getProduct.mockResolvedValue(mockProductNotFoundResponse); const result = await handlers.handleAnalyzeProduct(mockBarcodes.notFound); expect(result.content[0].text).toContain('Product not found for barcode'); }); it('should handle product without scores', async () => { const productWithoutScores = { status: 1, status_verbose: 'product found', product: { code: '123456789', product_name: 'Test Product', nutriments: { 'energy-kcal_100g': 200, 'fat_100g': 10, }, }, }; mockClient.getProduct.mockResolvedValue(productWithoutScores as any); const result = await handlers.handleAnalyzeProduct('123456789'); const text = result.content[0].text; expect(text).toContain('**Nutritional Analysis: Test Product**'); expect(text).not.toContain('**Scores:**'); expect(text).toContain('**Nutritional Breakdown:**'); }); it('should categorize nutrient levels correctly', async () => { const testProduct = { status: 1, status_verbose: 'product found', product: { code: '123456789', product_name: 'Test Product', nutriments: { 'energy-kcal_100g': 100, // low 'fat_100g': 25, // high 'sugars_100g': 10, // moderate 'salt_100g': 0.1, // low }, }, }; mockClient.getProduct.mockResolvedValue(testProduct as any); const result = await handlers.handleAnalyzeProduct('123456789'); const text = result.content[0].text; expect(text).toContain('• Energy: 100 kcal (low)'); expect(text).toContain('• Fat: 25g (high)'); expect(text).toContain('• Sugars: 10g (moderate)'); expect(text).toContain('• Salt: 0.1g (low)'); }); }); describe('handleCompareProducts', () => { it('should compare products with nutrition focus', async () => { const responses = [ mockNutellaProductResponse, mockHealthyYogurtProductResponse, ]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.yogurt], 'nutrition' ); const text = result.content[0].text; expect(text).toContain('**Product Comparison (nutrition)**'); expect(text).toContain('**1. Nutella**'); expect(text).toContain('Nutri-Score: E'); expect(text).toContain('**2. Greek Yogurt Natural**'); expect(text).toContain('Nutri-Score: B'); expect(text).toContain('Energy: 539 kcal'); expect(text).toContain('Energy: 97 kcal'); }); it('should compare products with environmental focus', async () => { const responses = [mockNutellaProductResponse, mockVeganProductResponse]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.oatDrink], 'environmental' ); const text = result.content[0].text; expect(text).toContain('**Product Comparison (environmental)**'); expect(text).toContain('Eco-Score: D'); expect(text).toContain('Eco-Score: B'); expect(text).toContain('Packaging: Plastic jar'); expect(text).toContain('Packaging: Tetra pak'); }); it('should compare products with processing focus', async () => { const responses = [mockNutellaProductResponse, mockHealthyYogurtProductResponse]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.yogurt], 'processing' ); const text = result.content[0].text; expect(text).toContain('**Product Comparison (processing)**'); expect(text).toContain('NOVA Group: 4 (Ultra-processed foods)'); expect(text).toContain('NOVA Group: 1 (Unprocessed or minimally processed foods)'); }); it('should compare products with ingredients focus', async () => { const responses = [mockNutellaProductResponse, mockVeganProductResponse]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.oatDrink], 'ingredients' ); const text = result.content[0].text; expect(text).toContain('**Product Comparison (ingredients)**'); expect(text).toContain('Sugar, palm oil, hazelnuts'); expect(text).toContain('Oat base (water, oats 10%)'); }); it('should handle insufficient valid products', async () => { const responses = [ mockNutellaProductResponse, mockProductNotFoundResponse, ]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.notFound] ); expect(result.content[0].text).toBe('Need at least 2 valid products to compare.'); }); it('should handle long ingredients text with truncation', async () => { const longIngredientsProduct = { ...mockNutellaProduct, ingredients_text: 'A'.repeat(300) + ' and more ingredients', }; const response = { ...mockNutellaProductResponse, product: longIngredientsProduct, }; const responses = [response, mockHealthyYogurtProductResponse]; mockClient.getProductsByBarcodes.mockResolvedValue(responses); const result = await handlers.handleCompareProducts( [mockBarcodes.nutella, mockBarcodes.yogurt], 'ingredients' ); const text = result.content[0].text; expect(text).toContain('...'); expect(text.length).toBeLessThan(1000); // Ensure truncation occurred }); }); describe('handleGetProductSuggestions', () => { it('should provide product suggestions with dietary preferences', async () => { const veganSearchResponse = { ...mockSearchResponse, products: [mockVeganProduct], }; mockClient.searchProducts.mockResolvedValue(veganSearchResponse); const result = await handlers.handleGetProductSuggestions({ category: 'beverages', dietary_preferences: ['vegan'], max_results: 5, }); expect(mockClient.searchProducts).toHaveBeenCalledWith({ categories: 'beverages', page_size: 10, // max_results * 2, but minimum sort_by: 'popularity', }); const text = result.content[0].text; expect(text).toContain('Product suggestions in beverages:'); expect(text).toContain('1. **Oat Drink Original**'); expect(text).toContain('Processing: NOVA 3'); }); it('should filter by minimum nutriscore', async () => { mockClient.searchProducts.mockResolvedValue(mockSearchResponse); await handlers.handleGetProductSuggestions({ category: 'snacks', min_nutriscore: 'b', }); expect(mockClient.searchProducts).toHaveBeenCalledWith({ categories: 'snacks', page_size: 20, sort_by: 'popularity', nutrition_grades: 'a,b', }); }); it('should handle multiple dietary preferences', async () => { const organicVeganResponse = { ...mockSearchResponse, products: [mockVeganProduct], }; mockClient.searchProducts.mockResolvedValue(organicVeganResponse); const result = await handlers.handleGetProductSuggestions({ category: 'plant-based-foods', dietary_preferences: ['vegan', 'organic', 'gluten-free'], max_results: 3, }); const text = result.content[0].text; expect(text).toContain('Product suggestions in plant-based-foods:'); }); it('should limit results to max_results', async () => { const manyProductsResponse = { ...mockSearchResponse, products: Array(10).fill(mockVeganProduct), }; mockClient.searchProducts.mockResolvedValue(manyProductsResponse); const result = await handlers.handleGetProductSuggestions({ category: 'beverages', max_results: 3, }); const text = result.content[0].text; const suggestionCount = (text.match(/^\d+\./gm) || []).length; expect(suggestionCount).toBe(3); }); it('should handle dietary preference filtering correctly', async () => { // Test internal filtering method by providing products that should/shouldn't match const mixedProducts = [ { ...mockVeganProduct, labels: 'Vegan, Organic', }, { ...mockNutellaProduct, labels: 'Chocolate, Sweet', }, ]; const mixedResponse = { ...mockSearchResponse, products: mixedProducts, }; mockClient.searchProducts.mockResolvedValue(mixedResponse as any); const result = await handlers.handleGetProductSuggestions({ category: 'foods', dietary_preferences: ['vegan'], }); const text = result.content[0].text; // Should only include the vegan product expect(text).toContain('Oat Drink Original'); expect(text).not.toContain('Nutella'); }); it('should format product suggestions with scores', async () => { mockClient.searchProducts.mockResolvedValue(mockSearchResponse); const result = await handlers.handleGetProductSuggestions({ category: 'beverages', }); const text = result.content[0].text; expect(text).toContain('Scores: Nutri-Score: E | Processing: NOVA 4'); expect(text).toContain('Scores: Nutri-Score: B | Processing: NOVA 1'); expect(text).toContain('Scores: Nutri-Score: B | Processing: NOVA 3 | Eco: B'); }); }); describe('private helper methods', () => { it('should extract key nutrients correctly', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleGetProduct(mockBarcodes.nutella); const text = result.content[0].text; // Verify the nutrient extraction logic expect(text).toContain('Energy: 539 kcal'); expect(text).toContain('Fat: 30.9g'); expect(text).toContain('Carbs: 57.5g'); expect(text).toContain('Sugars: 56.3g'); expect(text).toContain('Protein: 6.3g'); expect(text).toContain('Salt: 0.107g'); expect(text).not.toContain('Fiber: 0g'); // Should not show zero fiber }); it('should get score descriptions correctly', async () => { mockClient.getProduct.mockResolvedValue(mockNutellaProductResponse); const result = await handlers.handleAnalyzeProduct(mockBarcodes.nutella); const text = result.content[0].text; expect(text).toContain('Very poor nutritional quality'); expect(text).toContain('Ultra-processed foods'); expect(text).toContain('High environmental impact'); }); it('should handle unknown scores gracefully', async () => { const unknownScoresProduct = { status: 1, status_verbose: 'product found', product: { code: '123456789', product_name: 'Test Product', nutriscore_grade: 'x', nova_group: 99, ecoscore_grade: 'z', }, }; mockClient.getProduct.mockResolvedValue(unknownScoresProduct as any); const result = await handlers.handleAnalyzeProduct('123456789'); const text = result.content[0].text; expect(text).toContain('Unknown quality'); expect(text).toContain('Unknown processing level'); expect(text).toContain('Unknown environmental impact'); }); }); });

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