test-helpers.ts•9.82 kB
import { Product, ProductResponse, SearchResponse } from '../../src/types.js';
/**
* Test utilities for Open Food Facts MCP Server tests
*/
/**
* Creates a minimal valid product for testing
*/
export function createMinimalProduct(code: string, overrides: Partial<Product> = {}): Product {
return {
code,
...overrides,
};
}
/**
* Creates a complete product with all fields for testing
*/
export function createCompleteProduct(code: string, overrides: Partial<Product> = {}): Product {
return {
code,
product_name: 'Test Product',
brands: 'Test Brand',
categories: 'Test Category',
ingredients_text: 'Test ingredients',
nutriments: {
'energy-kcal_100g': 100,
'fat_100g': 5.0,
'carbohydrates_100g': 15.0,
'sugars_100g': 10.0,
'proteins_100g': 3.0,
'salt_100g': 0.5,
'fiber_100g': 2.0,
},
nutriscore_grade: 'b',
nova_group: 2,
ecoscore_grade: 'c',
image_url: 'https://example.com/image.jpg',
image_front_url: 'https://example.com/front.jpg',
quantity: '100g',
packaging: 'Box',
labels: 'Organic',
countries: 'Test Country',
manufacturing_places: 'Test Factory',
stores: 'Test Store',
created_datetime: '2023-01-01T00:00:00Z',
last_modified_datetime: 1672531200,
...overrides,
};
}
/**
* Creates a product response for testing
*/
export function createProductResponse(
status: number,
statusVerbose: string,
product?: Product
): ProductResponse {
return {
status,
status_verbose: statusVerbose,
...(product && { product }),
};
}
/**
* Creates a search response for testing
*/
export function createSearchResponse(
products: Product[],
overrides: Partial<SearchResponse> = {}
): SearchResponse {
return {
count: products.length,
page: 1,
page_count: Math.ceil(products.length / 20),
page_size: 20,
products,
...overrides,
};
}
/**
* Creates a product with specific nutritional profile for testing
*/
export function createNutritionalProduct(
code: string,
profile: 'healthy' | 'unhealthy' | 'average',
overrides: Partial<Product> = {}
): Product {
const profiles = {
healthy: {
nutriments: {
'energy-kcal_100g': 80,
'fat_100g': 2.0,
'saturated-fat_100g': 0.5,
'carbohydrates_100g': 10.0,
'sugars_100g': 3.0,
'proteins_100g': 8.0,
'salt_100g': 0.1,
'fiber_100g': 5.0,
},
nutriscore_grade: 'a',
nova_group: 1,
ecoscore_grade: 'a',
},
unhealthy: {
nutriments: {
'energy-kcal_100g': 550,
'fat_100g': 35.0,
'saturated-fat_100g': 15.0,
'carbohydrates_100g': 55.0,
'sugars_100g': 50.0,
'proteins_100g': 6.0,
'salt_100g': 2.0,
'fiber_100g': 0.0,
},
nutriscore_grade: 'e',
nova_group: 4,
ecoscore_grade: 'e',
},
average: {
nutriments: {
'energy-kcal_100g': 250,
'fat_100g': 12.0,
'saturated-fat_100g': 6.0,
'carbohydrates_100g': 30.0,
'sugars_100g': 20.0,
'proteins_100g': 5.0,
'salt_100g': 1.0,
'fiber_100g': 2.0,
},
nutriscore_grade: 'c',
nova_group: 3,
ecoscore_grade: 'c',
},
};
return createCompleteProduct(code, {
...profiles[profile],
...overrides,
});
}
/**
* Creates a product with specific dietary labels for testing
*/
export function createDietaryProduct(
code: string,
dietaryPreferences: string[],
overrides: Partial<Product> = {}
): Product {
const labelMap: Record<string, string[]> = {
vegan: ['Vegan', 'Plant-based'],
vegetarian: ['Vegetarian'],
'gluten-free': ['Gluten-free', 'Sans gluten'],
organic: ['Organic', 'Bio', 'Organic farming'],
'low-fat': ['Low fat', 'Light', 'Reduced fat'],
'low-sugar': ['Sugar-free', 'No added sugar', 'Low sugar'],
'high-protein': ['High protein', 'Rich in protein'],
};
const labels = dietaryPreferences
.flatMap(pref => labelMap[pref] || [])
.join(', ');
return createCompleteProduct(code, {
labels,
categories: 'Plant-based foods',
...overrides,
});
}
/**
* Generates a list of test barcodes
*/
export function generateTestBarcodes(count: number): string[] {
const barcodes: string[] = [];
for (let i = 0; i < count; i++) {
// Generate 13-digit barcodes starting with different prefixes
const prefix = (300 + (i % 100)).toString().padStart(3, '0');
const suffix = i.toString().padStart(10, '0');
barcodes.push(prefix + suffix);
}
return barcodes;
}
/**
* Creates mock axios response configuration
*/
export function createAxiosResponse(data: any, status = 200) {
return {
data,
status,
statusText: status === 200 ? 'OK' : 'Error',
headers: {},
config: {},
};
}
/**
* Validates that a text response contains expected nutritional information
*/
export function expectNutritionalInfo(text: string, product: Product): void {
if (product.product_name) {
expect(text).toContain(product.product_name);
}
if (product.brands) {
expect(text).toContain(product.brands);
}
if (product.nutriscore_grade) {
expect(text).toContain(product.nutriscore_grade.toUpperCase());
}
if (product.nova_group) {
expect(text).toContain(product.nova_group.toString());
}
if (product.ecoscore_grade) {
expect(text).toContain(product.ecoscore_grade.toUpperCase());
}
}
/**
* Validates that a text response contains proper formatting
*/
export function expectProperFormatting(text: string): void {
// Should contain markdown-style headers
expect(text).toMatch(/\*\*.*\*\*/);
// Should not contain undefined or null values
expect(text).not.toContain('undefined');
expect(text).not.toContain('null');
// Should not have excessive whitespace
expect(text).not.toMatch(/\n\n\n+/);
}
/**
* Creates a rate limit configuration for testing
*/
export function createRateLimitConfig(
products = 100,
search = 10,
facets = 2
) {
return {
products,
search,
facets,
};
}
/**
* Creates a client configuration for testing
*/
export function createTestConfig(overrides: Partial<any> = {}) {
return {
baseUrl: 'https://test.openfoodfacts.net',
userAgent: 'OpenFoodFactsMCP/1.0 (test)',
rateLimits: createRateLimitConfig(),
...overrides,
};
}
/**
* Simulates network delay for testing
*/
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Creates test data for comparison scenarios
*/
export function createComparisonProducts(): Product[] {
return [
createNutritionalProduct('1', 'healthy', {
product_name: 'Healthy Product',
brands: 'Health Brand',
}),
createNutritionalProduct('2', 'unhealthy', {
product_name: 'Unhealthy Product',
brands: 'Junk Brand',
}),
createNutritionalProduct('3', 'average', {
product_name: 'Average Product',
brands: 'Regular Brand',
}),
];
}
/**
* Creates test data for dietary filtering scenarios
*/
export function createDietaryProducts(): Product[] {
return [
createDietaryProduct('vegan1', ['vegan', 'organic'], {
product_name: 'Vegan Organic Product',
}),
createDietaryProduct('vegetarian1', ['vegetarian', 'gluten-free'], {
product_name: 'Vegetarian Gluten-Free Product',
}),
createDietaryProduct('regular1', [], {
product_name: 'Regular Product',
}),
createDietaryProduct('protein1', ['high-protein', 'low-fat'], {
product_name: 'High Protein Low Fat Product',
}),
];
}
/**
* Validates MCP tool response format
*/
export function expectValidMCPResponse(response: any): void {
expect(response).toHaveProperty('content');
expect(Array.isArray(response.content)).toBe(true);
expect(response.content.length).toBeGreaterThan(0);
response.content.forEach((item: any) => {
expect(item).toHaveProperty('type');
expect(item).toHaveProperty('text');
expect(typeof item.text).toBe('string');
expect(item.text.length).toBeGreaterThan(0);
});
}
/**
* Validates error response format
*/
export function expectValidErrorResponse(response: any): void {
expectValidMCPResponse(response);
expect(response.isError).toBe(true);
expect(response.content[0].text).toContain('Error:');
}
/**
* Creates mock environment variables for testing
*/
export function setupTestEnvironment(): void {
process.env.NODE_ENV = 'test';
process.env.OPEN_FOOD_FACTS_BASE_URL = 'https://test.openfoodfacts.net';
process.env.OPEN_FOOD_FACTS_USER_AGENT = 'OpenFoodFactsMCP/1.0 (test)';
}
/**
* Cleans up test environment
*/
export function cleanupTestEnvironment(): void {
delete process.env.OPEN_FOOD_FACTS_BASE_URL;
delete process.env.OPEN_FOOD_FACTS_USER_AGENT;
}
/**
* Measures execution time of async functions
*/
export async function measureTime<T>(
fn: () => Promise<T>
): Promise<{ result: T; duration: number }> {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
return { result, duration };
}
/**
* Retries an async function with exponential backoff
*/
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 100
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxRetries) {
throw lastError;
}
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}