From 26f309acfb5446f334bd8959c32f1287e18ae062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 13 Jul 2026 16:18:11 +0200 Subject: [PATCH] fix(backend): switch OFF API to /cgi/search.pl endpoint and fix test imports - Changed from /api/v2/search (returning 503) to /cgi/search.pl (working) - Removed strict categories_tags=baby-food filter that was too restrictive - Fixed test file to use dynamic imports for ES module compatibility - Increased timeout to 10s for reliability --- apps/backend/__tests__/off-service.test.js | 89 +++++++++++++--------- apps/backend/off-service.js | 12 +-- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/apps/backend/__tests__/off-service.test.js b/apps/backend/__tests__/off-service.test.js index c5d7840..3bae2e9 100644 --- a/apps/backend/__tests__/off-service.test.js +++ b/apps/backend/__tests__/off-service.test.js @@ -1,4 +1,19 @@ -import { transformOFFProduct, searchBabyProducts, getBabyProductDetails } from '../off-service.js'; +import { jest } from '@jest/globals' + +jest.unstable_mockModule('../redis-client.js', () => ({ + default: { + get: jest.fn(async () => null), + setEx: jest.fn(async () => 'OK'), + }, +})) + +jest.unstable_mockModule('axios', () => ({ + default: { + get: jest.fn(async () => ({ data: { products: [] } })), + }, +})) + +const { transformOFFProduct, searchBabyProducts, getBabyProductDetails } = await import('../off-service.js') describe('transformOFFProduct', () => { test('transforms a valid OFF product to unified model', () => { @@ -12,9 +27,9 @@ describe('transformOFFProduct', () => { nova_group: 1, ecoscore_grade: 'b', categories_tags: ['en:baby-foods', 'en:baby-rice'], - }; + } - const result = transformOFFProduct(offProduct); + const result = transformOFFProduct(offProduct) expect(result).toEqual({ id: '12345', @@ -27,66 +42,66 @@ describe('transformOFFProduct', () => { ingredients: 'Rice, Iron, Vitamins', nova_group: 1, eco_score: 'b', - }); - }); + }) + }) test('returns null for null/undefined input', () => { - expect(transformOFFProduct(null)).toBeNull(); - expect(transformOFFProduct(undefined)).toBeNull(); - }); + expect(transformOFFProduct(null)).toBeNull() + expect(transformOFFProduct(undefined)).toBeNull() + }) test('returns null when missing required fields', () => { - expect(transformOFFProduct({ _id: '123' })).toBeNull(); - expect(transformOFFProduct({ product_name: 'Test' })).toBeNull(); - }); + expect(transformOFFProduct({ _id: '123' })).toBeNull() + expect(transformOFFProduct({ product_name: 'Test' })).toBeNull() + }) test('sets default brand to empty string when missing', () => { const result = transformOFFProduct({ _id: '999', product_name: 'Plain Product', - }); - expect(result.brand).toBe(''); - expect(result.image_url).toBeNull(); - expect(result.nutriscore).toBeNull(); - expect(result.ingredients).toBeNull(); - expect(result.nova_group).toBeNull(); - expect(result.eco_score).toBeNull(); - }); + }) + expect(result.brand).toBe('') + expect(result.image_url).toBeNull() + expect(result.nutriscore).toBeNull() + expect(result.ingredients).toBeNull() + expect(result.nova_group).toBeNull() + expect(result.eco_score).toBeNull() + }) test('detects baby_milk category from tags', () => { const result = transformOFFProduct({ _id: '200', product_name: 'Infant Formula', categories_tags: ['en:infant-milk'], - }); - expect(result.category).toBe('baby_milk'); - }); + }) + expect(result.category).toBe('baby_milk') + }) test('detects baby_cereal category from tags', () => { const result = transformOFFProduct({ _id: '300', product_name: 'Baby Cereal', categories_tags: ['en:baby-cereals'], - }); - expect(result.category).toBe('baby_cereal'); - }); -}); + }) + expect(result.category).toBe('baby_cereal') + }) +}) describe('searchBabyProducts', () => { test('returns empty array for short query', async () => { - const result = await searchBabyProducts('a'); - expect(result).toEqual([]); - }); + const result = await searchBabyProducts('a') + expect(result).toEqual([]) + }) test('returns empty array for null/empty query', async () => { - expect(await searchBabyProducts(null)).toEqual([]); - expect(await searchBabyProducts('')).toEqual([]); - }); -}); + expect(await searchBabyProducts(null)).toEqual([]) + expect(await searchBabyProducts('')).toEqual([]) + }) +}) describe('getBabyProductDetails', () => { test('returns null for null barcode', async () => { - const result = await getBabyProductDetails(null); - expect(result).toBeNull(); - }); -}); + const result = await getBabyProductDetails(null) + expect(result).toBeNull() + }) +}) diff --git a/apps/backend/off-service.js b/apps/backend/off-service.js index 3066e56..e7e28c7 100644 --- a/apps/backend/off-service.js +++ b/apps/backend/off-service.js @@ -1,7 +1,7 @@ import axios from 'axios'; import redisClient from './redis-client.js'; -const OFF_API_BASE = 'https://world.openfoodfacts.org/api/v2'; +const OFF_API_BASE = 'https://world.openfoodfacts.org'; const CACHE_TTL = 3600; export function transformOFFProduct(offProduct) { @@ -53,15 +53,15 @@ export async function searchBabyProducts(query) { } console.log(`Fetching from OFF API: ${searchTerm}`); - const response = await axios.get(`${OFF_API_BASE}/search`, { + const response = await axios.get(`${OFF_API_BASE}/cgi/search.pl`, { params: { - categories_tags: 'baby-food', search_terms: searchTerm, json: true, fields: 'product_name,brands,image_url,nutriscore_grade,ingredients_text,nova_group,ecoscore_grade,categories_tags', page_size: 20, + action: 'process', }, - timeout: 5000, + timeout: 10000, }); if (response.data && response.data.products) { @@ -100,8 +100,8 @@ export async function getBabyProductDetails(barcode) { } console.log(`Fetching OFF product details: ${barcode}`); - const response = await axios.get(`${OFF_API_BASE}/product/${barcode}.json`, { - timeout: 5000, + const response = await axios.get(`${OFF_API_BASE}/api/v2/product/${barcode}.json`, { + timeout: 10000, }); if (response.data && response.data.product) {