e2e.test.ts•17.7 kB
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from '@jest/globals';
import { OpenFoodFactsClient } from '../src/client.js';
import { ToolHandlers } from '../src/handlers.js';
import { OpenFoodFactsConfig } from '../src/types.js';
import { createMockServer, MockOpenFoodFactsServer } from './utils/mock-server.js';
import {
createTestConfig,
expectValidMCPResponse,
expectValidErrorResponse,
expectNutritionalInfo,
expectProperFormatting
} from './utils/test-helpers.js';
import {
mockNutellaProduct,
mockHealthyYogurtProduct,
mockVeganProduct,
mockNutellaProductResponse,
mockHealthyYogurtProductResponse,
mockVeganProductResponse,
mockBarcodes
} from './fixtures/products.js';
/**
* End-to-End tests that simulate real usage scenarios
*/
describe('End-to-End Scenarios', () => {
let mockServer: MockOpenFoodFactsServer;
let client: OpenFoodFactsClient;
let handlers: ToolHandlers;
let config: OpenFoodFactsConfig;
beforeAll(() => {
config = createTestConfig();
client = new OpenFoodFactsClient(config);
handlers = new ToolHandlers(client);
});
beforeEach(() => {
mockServer = createMockServer();
});
afterEach(() => {
mockServer.cleanup();
});
describe('Complete Product Lookup Workflow', () => {
it('should handle complete product analysis workflow', async () => {
mockServer
.mockGetProduct(mockBarcodes.nutella, mockNutellaProductResponse)
.mockGetProduct(mockBarcodes.yogurt, mockHealthyYogurtProductResponse);
// Step 1: Basic product lookup
const productResult = await handlers.handleGetProduct(mockBarcodes.nutella);
expectValidMCPResponse(productResult);
expectNutritionalInfo(productResult.content[0].text, mockNutellaProduct);
expectProperFormatting(productResult.content[0].text);
expect(productResult.content[0].text).toContain('**Nutella**');
expect(productResult.content[0].text).toContain('Nutri-Score: E');
// Step 2: Detailed nutritional analysis
const analysisResult = await handlers.handleAnalyzeProduct(mockBarcodes.nutella);
expectValidMCPResponse(analysisResult);
expect(analysisResult.content[0].text).toContain('**Nutritional Analysis: Nutella**');
expect(analysisResult.content[0].text).toContain('Very poor nutritional quality');
expect(analysisResult.content[0].text).toContain('Ultra-processed foods');
expect(analysisResult.content[0].text).toContain('high');
// Step 3: Product comparison
const comparisonResult = await handlers.handleCompareProducts(
[mockBarcodes.nutella, mockBarcodes.yogurt],
'nutrition'
);
expectValidMCPResponse(comparisonResult);
expect(comparisonResult.content[0].text).toContain('**Product Comparison (nutrition)**');
expect(comparisonResult.content[0].text).toContain('Nutella');
expect(comparisonResult.content[0].text).toContain('Greek Yogurt');
expect(comparisonResult.content[0].text).toContain('Nutri-Score: E');
expect(comparisonResult.content[0].text).toContain('Nutri-Score: B');
expect(mockServer.isDone()).toBe(true);
});
it('should handle search to analysis workflow', async () => {
const searchResponse = {
count: 2,
page: 1,
page_count: 1,
page_size: 20,
products: [mockNutellaProduct, mockHealthyYogurtProduct],
};
mockServer
.mockSearchAny(searchResponse)
.mockGetProduct(mockBarcodes.nutella, mockNutellaProductResponse);
// Step 1: Search for products
const searchResult = await handlers.handleSearchProducts({
search: 'chocolate',
nutrition_grades: 'a,b,c,d,e',
});
expectValidMCPResponse(searchResult);
expect(searchResult.content[0].text).toContain('Found 2 products');
expect(searchResult.content[0].text).toContain('Nutella');
expect(searchResult.content[0].text).toContain('Greek Yogurt');
// Step 2: Analyze specific product found in search
const analysisResult = await handlers.handleAnalyzeProduct(mockBarcodes.nutella);
expectValidMCPResponse(analysisResult);
expect(analysisResult.content[0].text).toContain('**Nutritional Analysis: Nutella**');
expect(mockServer.isDone()).toBe(true);
});
});
describe('Dietary Recommendations Workflow', () => {
it('should provide complete dietary guidance workflow', async () => {
const veganSearchResponse = {
count: 1,
page: 1,
page_count: 1,
page_size: 20,
products: [mockVeganProduct],
};
mockServer
.mockSearchAny(veganSearchResponse)
.mockGetProduct(mockBarcodes.oatDrink, mockVeganProductResponse);
// Step 1: Get dietary suggestions
const suggestionsResult = await handlers.handleGetProductSuggestions({
category: 'beverages',
dietary_preferences: ['vegan', 'organic'],
min_nutriscore: 'b',
max_results: 5,
});
expectValidMCPResponse(suggestionsResult);
expect(suggestionsResult.content[0].text).toContain('Product suggestions in beverages');
expect(suggestionsResult.content[0].text).toContain('Oat Drink Original');
// Step 2: Analyze suggested product
const analysisResult = await handlers.handleAnalyzeProduct(mockBarcodes.oatDrink);
expectValidMCPResponse(analysisResult);
expect(analysisResult.content[0].text).toContain('**Nutritional Analysis: Oat Drink Original**');
expect(analysisResult.content[0].text).toContain('Good nutritional quality');
expect(mockServer.isDone()).toBe(true);
});
it('should handle healthy alternatives workflow', async () => {
const healthySearchResponse = {
count: 2,
page: 1,
page_count: 1,
page_size: 20,
products: [mockHealthyYogurtProduct, mockVeganProduct],
};
mockServer
.mockSearchAny(healthySearchResponse)
.mockGetProduct(mockBarcodes.yogurt, mockHealthyYogurtProductResponse)
.mockGetProduct(mockBarcodes.oatDrink, mockVeganProductResponse);
// Step 1: Search for healthy alternatives
const searchResult = await handlers.handleSearchProducts({
categories: 'dairy',
nutrition_grades: 'a,b',
nova_groups: '1,2',
});
expectValidMCPResponse(searchResult);
expect(searchResult.content[0].text).toContain('Greek Yogurt');
expect(searchResult.content[0].text).toContain('Oat Drink');
// Step 2: Compare healthy options
const comparisonResult = await handlers.handleCompareProducts(
[mockBarcodes.yogurt, mockBarcodes.oatDrink],
'nutrition'
);
expectValidMCPResponse(comparisonResult);
expect(comparisonResult.content[0].text).toContain('Greek Yogurt Natural');
expect(comparisonResult.content[0].text).toContain('Oat Drink Original');
expect(mockServer.isDone()).toBe(true);
});
});
describe('Error Handling Scenarios', () => {
it('should gracefully handle mixed success/failure scenarios', async () => {
mockServer
.mockGetProduct(mockBarcodes.nutella, mockNutellaProductResponse)
.mockGetProductNotFound(mockBarcodes.notFound)
.mockGetProductError('error123', 500);
// Test successful lookup
const successResult = await handlers.handleGetProduct(mockBarcodes.nutella);
expectValidMCPResponse(successResult);
expect(successResult.content[0].text).toContain('Nutella');
// Test not found
const notFoundResult = await handlers.handleGetProduct(mockBarcodes.notFound);
expectValidMCPResponse(notFoundResult);
expect(notFoundResult.content[0].text).toContain('Product not found');
// Test server error - should be handled by client
try {
await handlers.handleGetProduct('error123');
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('Failed to fetch');
}
expect(mockServer.isDone()).toBe(true);
});
it('should handle network failures gracefully', async () => {
mockServer.mockGetProductNetworkError(mockBarcodes.nutella);
try {
await handlers.handleGetProduct(mockBarcodes.nutella);
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
expect(mockServer.isDone()).toBe(true);
});
it('should handle empty search results appropriately', async () => {
const emptySearchResponse = {
count: 0,
page: 1,
page_count: 0,
page_size: 20,
products: [],
};
mockServer.mockSearchAny(emptySearchResponse);
const result = await handlers.handleSearchProducts({
search: 'nonexistent_product_xyz',
});
expectValidMCPResponse(result);
expect(result.content[0].text).toBe('No products found matching your search criteria.');
expect(mockServer.isDone()).toBe(true);
});
});
describe('Performance and Rate Limiting Scenarios', () => {
it('should handle batch operations efficiently', async () => {
const barcodes = [mockBarcodes.nutella, mockBarcodes.yogurt, mockBarcodes.oatDrink];
mockServer
.mockGetProduct(mockBarcodes.nutella, mockNutellaProductResponse)
.mockGetProduct(mockBarcodes.yogurt, mockHealthyYogurtProductResponse)
.mockGetProduct(mockBarcodes.oatDrink, mockVeganProductResponse);
const startTime = Date.now();
const result = await handlers.handleCompareProducts(barcodes, 'nutrition');
const duration = Date.now() - startTime;
expectValidMCPResponse(result);
expect(result.content[0].text).toContain('**Product Comparison (nutrition)**');
expect(result.content[0].text).toContain('Nutella');
expect(result.content[0].text).toContain('Greek Yogurt');
expect(result.content[0].text).toContain('Oat Drink');
// Should complete reasonably quickly (allowing for network delays in tests)
expect(duration).toBeLessThan(5000);
expect(mockServer.isDone()).toBe(true);
});
it('should handle rate limit scenarios', async () => {
const rateLimitedClient = new OpenFoodFactsClient({
...config,
rateLimits: { products: 1, search: 1, facets: 1 },
});
const rateLimitedHandlers = new ToolHandlers(rateLimitedClient);
mockServer.mockGetProduct(mockBarcodes.nutella, mockNutellaProductResponse);
// First request should succeed
const result1 = await rateLimitedHandlers.handleGetProduct(mockBarcodes.nutella);
expectValidMCPResponse(result1);
// Second request should fail due to rate limit
try {
await rateLimitedHandlers.handleGetProduct(mockBarcodes.yogurt);
fail('Should have thrown rate limit error');
} catch (error) {
expect((error as Error).message).toContain('Rate limit exceeded');
}
});
});
describe('Complex Multi-Step Workflows', () => {
it('should handle comprehensive product research workflow', async () => {
// Setup mocks for a complete product research session
const chocolateSearchResponse = {
count: 100,
page: 1,
page_count: 5,
page_size: 20,
products: [mockNutellaProduct, mockHealthyYogurtProduct],
};
const healthyChocolateResponse = {
count: 10,
page: 1,
page_count: 1,
page_size: 20,
products: [mockHealthyYogurtProduct],
};
mockServer
.mockSearch({ search: 'chocolate' }, chocolateSearchResponse)
.mockSearch({
categories: 'chocolates',
nutrition_grades: 'a,b'
}, healthyChocolateResponse)
.mockMultipleProducts([
mockBarcodes.nutella,
mockBarcodes.yogurt
]);
// Step 1: Initial search for chocolate products
const initialSearch = await handlers.handleSearchProducts({
search: 'chocolate',
});
expectValidMCPResponse(initialSearch);
expect(initialSearch.content[0].text).toContain('Found 100 products');
// Step 2: Refine search for healthier options
const refinedSearch = await handlers.handleSearchProducts({
categories: 'chocolates',
nutrition_grades: 'a,b',
});
expectValidMCPResponse(refinedSearch);
expect(refinedSearch.content[0].text).toContain('Found 10 products');
// Step 3: Compare products from search results
const comparison = await handlers.handleCompareProducts(
[mockBarcodes.nutella, mockBarcodes.yogurt],
'nutrition'
);
expectValidMCPResponse(comparison);
expect(comparison.content[0].text).toContain('**Product Comparison (nutrition)**');
// Step 4: Detailed analysis of the better option
const detailedAnalysis = await handlers.handleAnalyzeProduct(mockBarcodes.yogurt);
expectValidMCPResponse(detailedAnalysis);
expect(detailedAnalysis.content[0].text).toContain('Good nutritional quality');
expect(mockServer.isDone()).toBe(true);
});
it('should handle dietary planning workflow', async () => {
// Mock responses for dietary planning scenario
const beverageResponse = {
count: 20,
page: 1,
page_count: 1,
page_size: 20,
products: [mockVeganProduct],
};
const snackResponse = {
count: 15,
page: 1,
page_count: 1,
page_size: 20,
products: [mockHealthyYogurtProduct],
};
mockServer
.mockSearch({ categories: 'beverages' }, beverageResponse)
.mockSearch({ categories: 'snacks' }, snackResponse)
.mockGetProduct(mockBarcodes.oatDrink, mockVeganProductResponse)
.mockGetProduct(mockBarcodes.yogurt, mockHealthyYogurtProductResponse);
// Step 1: Find vegan beverages
const beverageSuggestions = await handlers.handleGetProductSuggestions({
category: 'beverages',
dietary_preferences: ['vegan'],
max_results: 5,
});
expectValidMCPResponse(beverageSuggestions);
expect(beverageSuggestions.content[0].text).toContain('Product suggestions in beverages');
// Step 2: Find healthy snacks
const snackSuggestions = await handlers.handleGetProductSuggestions({
category: 'snacks',
dietary_preferences: ['high-protein'],
min_nutriscore: 'c',
max_results: 3,
});
expectValidMCPResponse(snackSuggestions);
expect(snackSuggestions.content[0].text).toContain('Product suggestions in snacks');
// Step 3: Compare final selections
const finalComparison = await handlers.handleCompareProducts(
[mockBarcodes.oatDrink, mockBarcodes.yogurt],
'nutrition'
);
expectValidMCPResponse(finalComparison);
expect(finalComparison.content[0].text).toContain('Oat Drink Original');
expect(finalComparison.content[0].text).toContain('Greek Yogurt Natural');
expect(mockServer.isDone()).toBe(true);
});
});
describe('Edge Cases and Boundary Conditions', () => {
it('should handle products with minimal data', async () => {
const minimalProductResponse = {
status: 1,
status_verbose: 'product found',
product: {
code: 'minimal123',
product_name: 'Minimal Product',
},
};
mockServer.mockGetProduct('minimal123', minimalProductResponse as any);
const result = await handlers.handleGetProduct('minimal123');
expectValidMCPResponse(result);
expect(result.content[0].text).toContain('**Minimal Product**');
expect(result.content[0].text).not.toContain('**Scores:**');
expect(result.content[0].text).not.toContain('**Nutrition**');
expect(mockServer.isDone()).toBe(true);
});
it('should handle products with extreme nutritional values', async () => {
const extremeProduct = {
status: 1,
status_verbose: 'product found',
product: {
code: 'extreme123',
product_name: 'Extreme Product',
nutriments: {
'energy-kcal_100g': 999,
'fat_100g': 99.9,
'sugars_100g': 99.9,
'salt_100g': 10.0,
},
nutriscore_grade: 'e',
nova_group: 4,
},
};
mockServer.mockGetProduct('extreme123', extremeProduct as any);
const result = await handlers.handleAnalyzeProduct('extreme123');
expectValidMCPResponse(result);
expect(result.content[0].text).toContain('**Nutritional Analysis: Extreme Product**');
expect(result.content[0].text).toContain('(high)');
expect(result.content[0].text).toContain('Very poor nutritional quality');
expect(mockServer.isDone()).toBe(true);
});
it('should handle large search result sets', async () => {
const largeSearchResponse = {
count: 5000,
page: 1,
page_count: 250,
page_size: 20,
products: Array(20).fill(mockNutellaProduct),
};
mockServer.mockSearchAny(largeSearchResponse);
const result = await handlers.handleSearchProducts({
search: 'popular_category',
});
expectValidMCPResponse(result);
expect(result.content[0].text).toContain('Found 5000 products');
expect(result.content[0].text).toContain('page 1 of 250');
expect(mockServer.isDone()).toBe(true);
});
});
});