Add Open Food Facts barcode lookup workflow
This commit is contained in:
1 parent
7f24948317
commit
cf9a4f010d
10 files changed
+3187
-18
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { fetchProductFromOFF } from './openFoodFacts';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe('fetchProductFromOFF', () => {
|
||||
const endpoint = 'https://world.openfoodfacts.org/api/v2/product/123456.json';
|
||||
|
||||
it('returns parsed product data when available', async () => {
|
||||
server.use(
|
||||
http.get(endpoint, () =>
|
||||
HttpResponse.json({
|
||||
status: 1,
|
||||
product: {
|
||||
product_name: 'Sparkling Water',
|
||||
brands: 'Acme',
|
||||
quantity: '500ml',
|
||||
image_url: 'https://example.com/water.png',
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await fetchProductFromOFF('123456');
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'Sparkling Water',
|
||||
brand: 'Acme',
|
||||
quantity: '500ml',
|
||||
image: 'https://example.com/water.png',
|
||||
source: 'openfoodfacts',
|
||||
});
|
||||
});
|
||||
|
||||
it('coerces missing optional fields to null', async () => {
|
||||
server.use(
|
||||
http.get(endpoint, () =>
|
||||
HttpResponse.json({
|
||||
status: 1,
|
||||
product: {
|
||||
product_name: 'Chocolate Bar',
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await fetchProductFromOFF('123456');
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'Chocolate Bar',
|
||||
brand: null,
|
||||
quantity: null,
|
||||
image: null,
|
||||
source: 'openfoodfacts',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when the product is not found', async () => {
|
||||
server.use(http.get(endpoint, () => HttpResponse.json({ status: 0 })));
|
||||
|
||||
const result = await fetchProductFromOFF('123456');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null on network error', async () => {
|
||||
server.use(http.get(endpoint, () => HttpResponse.error()));
|
||||
|
||||
const result = await fetchProductFromOFF('123456');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export interface ExternalProductInfo {
|
||||
name: string | null;
|
||||
brand?: string | null;
|
||||
quantity?: string | null;
|
||||
image?: string | null;
|
||||
source: 'openfoodfacts';
|
||||
}
|
||||
|
||||
const BASE_URL = 'https://world.openfoodfacts.org/api/v2/product';
|
||||
const REQUEST_TIMEOUT_MS = 5000;
|
||||
|
||||
const isOffline = () => typeof navigator !== 'undefined' && 'onLine' in navigator && !navigator.onLine;
|
||||
|
||||
export const fetchProductFromOFF = async (barcode: string): Promise<ExternalProductInfo | null> => {
|
||||
if (!barcode || isOffline()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/${barcode}.json`, { signal: controller.signal });
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = (await response.json()) as {
|
||||
status?: number;
|
||||
product?: {
|
||||
product_name?: string | null;
|
||||
brands?: string | null;
|
||||
quantity?: string | null;
|
||||
image_url?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
if (data?.status !== 1 || !data.product || !data.product.product_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: data.product.product_name ?? null,
|
||||
brand: data.product.brands ?? null,
|
||||
quantity: data.product.quantity ?? null,
|
||||
image: data.product.image_url ?? null,
|
||||
source: 'openfoodfacts',
|
||||
} satisfies ExternalProductInfo;
|
||||
} catch (error) {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user