// src/services/upcService.server.test.ts import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { Logger } from 'pino'; import { createMockLogger } from '../tests/utils/mockLogger'; import type { UpcScanSource, UpcExternalLookupRecord, UpcExternalSource } from '../types/upc'; // Mock dependencies vi.mock('./db/index.db', () => ({ upcRepo: { recordScan: vi.fn(), findProductByUpc: vi.fn(), findExternalLookup: vi.fn(), upsertExternalLookup: vi.fn(), linkUpcToProduct: vi.fn(), getScanHistory: vi.fn(), getUserScanStats: vi.fn(), getScanById: vi.fn(), }, })); vi.mock('../config/env', () => ({ config: { upc: { upcItemDbApiKey: undefined, barcodeLookupApiKey: undefined, }, }, isUpcItemDbConfigured: false, isBarcodeLookupConfigured: false, })); vi.mock('./logger.server', () => ({ logger: { child: vi.fn().mockReturnThis(), debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); // Mock global fetch const mockFetch = vi.fn(); global.fetch = mockFetch; // Import after mocks are set up import { isValidUpcCode, normalizeUpcCode, detectBarcodeFromImage, lookupExternalUpc, scanUpc, lookupUpc, linkUpcToProduct, getScanHistory, getScanStats, getScanById, } from './upcService.server'; import { upcRepo } from './db/index.db'; // Helper to create mock UpcExternalLookupRecord function createMockExternalLookupRecord( overrides: Partial = {}, ): UpcExternalLookupRecord { return { lookup_id: 1, upc_code: '012345678905', product_name: null, brand_name: null, category: null, description: null, image_url: null, external_source: 'openfoodfacts' as UpcExternalSource, lookup_data: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides, }; } // Helper to create mock ProductRow (from db layer - matches upc.db.ts) interface ProductRow { product_id: number; name: string; brand_id: number | null; category_id: number | null; description: string | null; size: string | null; upc_code: string | null; master_item_id: number | null; created_at: string; updated_at: string; } function createMockProductRow(overrides: Partial = {}): ProductRow { return { product_id: 1, name: 'Test Product', brand_id: null, category_id: null, description: null, size: null, upc_code: '012345678905', master_item_id: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides, }; } describe('upcService.server', () => { let mockLogger: Logger; beforeEach(() => { vi.clearAllMocks(); mockLogger = createMockLogger(); mockFetch.mockReset(); }); afterEach(() => { vi.resetAllMocks(); }); describe('isValidUpcCode', () => { it('should return true for valid 12-digit UPC-A', () => { expect(isValidUpcCode('012345678905')).toBe(true); }); it('should return true for valid 8-digit UPC-E', () => { expect(isValidUpcCode('01234567')).toBe(true); }); it('should return true for valid 13-digit EAN-13', () => { expect(isValidUpcCode('5901234123457')).toBe(true); }); it('should return true for valid 8-digit EAN-8', () => { expect(isValidUpcCode('96385074')).toBe(true); }); it('should return true for valid 14-digit GTIN-14', () => { expect(isValidUpcCode('00012345678905')).toBe(true); }); it('should return false for code with less than 8 digits', () => { expect(isValidUpcCode('1234567')).toBe(false); }); it('should return false for code with more than 14 digits', () => { expect(isValidUpcCode('123456789012345')).toBe(false); }); it('should return false for code with non-numeric characters', () => { expect(isValidUpcCode('01234567890A')).toBe(false); }); it('should return false for empty string', () => { expect(isValidUpcCode('')).toBe(false); }); }); describe('normalizeUpcCode', () => { it('should remove spaces from UPC code', () => { expect(normalizeUpcCode('012 345 678 905')).toBe('012345678905'); }); it('should remove dashes from UPC code', () => { expect(normalizeUpcCode('012-345-678-905')).toBe('012345678905'); }); it('should remove mixed spaces and dashes', () => { expect(normalizeUpcCode('012-345 678-905')).toBe('012345678905'); }); it('should return unchanged if no spaces or dashes', () => { expect(normalizeUpcCode('012345678905')).toBe('012345678905'); }); }); describe('detectBarcodeFromImage', () => { it('should return not implemented error', async () => { const result = await detectBarcodeFromImage('base64imagedata', mockLogger); expect(result.detected).toBe(false); expect(result.upc_code).toBeNull(); expect(result.error).toBe( 'Barcode detection from images is not yet implemented. Please use manual entry.', ); }); }); describe('lookupExternalUpc', () => { it('should return product info from Open Food Facts on success', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { product_name: 'Test Product', brands: 'Test Brand', categories_tags: ['en:snacks'], ingredients_text: 'Test ingredients', image_url: 'https://example.com/image.jpg', }, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).not.toBeNull(); expect(result?.name).toBe('Test Product'); expect(result?.brand).toBe('Test Brand'); expect(result?.source).toBe('openfoodfacts'); }); it('should return null when Open Food Facts returns status 0', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should return null when Open Food Facts request fails', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should return null on network error', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should use generic_name when product_name is missing', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { generic_name: 'Generic Product Name', brands: null, }, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.name).toBe('Generic Product Name'); }); }); describe('scanUpc', () => { it('should scan with manual entry and return product from database', async () => { const mockProduct = { product_id: 1, name: 'Test Product', brand: 'Test Brand', category: 'Snacks', description: null, size: '100g', upc_code: '012345678905', image_url: null, master_item_id: null, }; vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(mockProduct); vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 1, user_id: 'user-1', upc_code: '012345678905', product_id: 1, scan_source: 'manual_entry', scan_confidence: 1.0, raw_image_path: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await scanUpc( 'user-1', { upc_code: '012345678905', scan_source: 'manual_entry' }, mockLogger, ); expect(result.upc_code).toBe('012345678905'); expect(result.product).toEqual(mockProduct); expect(result.lookup_successful).toBe(true); expect(result.is_new_product).toBe(false); expect(result.confidence).toBe(1.0); }); it('should scan with manual entry and perform external lookup when not in database', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 2, user_id: 'user-1', upc_code: '012345678905', product_id: null, scan_source: 'manual_entry', scan_confidence: 1.0, raw_image_path: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { product_name: 'External Product', brands: 'External Brand', }, }), }); const result = await scanUpc( 'user-1', { upc_code: '012345678905', scan_source: 'manual_entry' }, mockLogger, ); expect(result.product).toBeNull(); expect(result.external_lookup).not.toBeNull(); expect(result.external_lookup?.name).toBe('External Product'); expect(result.is_new_product).toBe(true); }); it('should use cached external lookup when available', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({ lookup_id: 1, upc_code: '012345678905', product_name: 'Cached Product', brand_name: 'Cached Brand', category: 'Cached Category', description: null, image_url: null, external_source: 'openfoodfacts', lookup_data: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 3, user_id: 'user-1', upc_code: '012345678905', product_id: null, scan_source: 'manual_entry', scan_confidence: 1.0, raw_image_path: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await scanUpc( 'user-1', { upc_code: '012345678905', scan_source: 'manual_entry' }, mockLogger, ); expect(result.external_lookup?.name).toBe('Cached Product'); expect(mockFetch).not.toHaveBeenCalled(); }); it('should throw error for invalid UPC code format', async () => { await expect( scanUpc('user-1', { upc_code: 'invalid', scan_source: 'manual_entry' }, mockLogger), ).rejects.toThrow('Invalid UPC code format. UPC codes must be 8-14 digits.'); }); it('should throw error when neither upc_code nor image_base64 provided', async () => { await expect( scanUpc('user-1', { scan_source: 'manual_entry' } as any, mockLogger), ).rejects.toThrow('Either upc_code or image_base64 must be provided.'); }); it('should record failed scan when image detection fails', async () => { vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 4, user_id: 'user-1', upc_code: 'DETECTION_FAILED', product_id: null, scan_source: 'image_upload', scan_confidence: 0, raw_image_path: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await scanUpc( 'user-1', { image_base64: 'base64data', scan_source: 'image_upload' }, mockLogger, ); expect(result.lookup_successful).toBe(false); expect(result.confidence).toBe(0); }); }); describe('lookupUpc', () => { it('should return product from database when found', async () => { const mockProduct = { product_id: 1, name: 'Test Product', brand: 'Test Brand', category: 'Snacks', description: null, size: '100g', upc_code: '012345678905', image_url: null, master_item_id: null, }; vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(mockProduct); const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger); expect(result.found).toBe(true); expect(result.product).toEqual(mockProduct); expect(result.from_cache).toBe(false); }); it('should return cached external lookup when available', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({ lookup_id: 1, upc_code: '012345678905', product_name: 'Cached Product', brand_name: null, category: null, description: null, image_url: null, external_source: 'openfoodfacts', lookup_data: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger); expect(result.found).toBe(true); expect(result.from_cache).toBe(true); expect(result.external_lookup?.name).toBe('Cached Product'); }); it('should return cached unsuccessful lookup', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({ lookup_id: 1, upc_code: '012345678905', product_name: null, brand_name: null, category: null, description: null, image_url: null, external_source: 'unknown', lookup_data: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger); expect(result.found).toBe(false); expect(result.from_cache).toBe(true); }); it('should perform fresh external lookup when force_refresh is true', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { product_name: 'Fresh External Product', brands: 'Fresh Brand', }, }), }); const result = await lookupUpc({ upc_code: '012345678905', force_refresh: true }, mockLogger); expect(result.from_cache).toBe(false); expect(result.external_lookup?.name).toBe('Fresh External Product'); expect(upcRepo.findExternalLookup).not.toHaveBeenCalled(); }); it('should throw error for invalid UPC code', async () => { await expect(lookupUpc({ upc_code: 'invalid' }, mockLogger)).rejects.toThrow( 'Invalid UPC code format. UPC codes must be 8-14 digits.', ); }); it('should normalize UPC code before lookup', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0 }), }); const result = await lookupUpc({ upc_code: '012-345-678-905' }, mockLogger); expect(result.upc_code).toBe('012345678905'); }); }); describe('linkUpcToProduct', () => { it('should link UPC code to product successfully', async () => { vi.mocked(upcRepo.linkUpcToProduct).mockResolvedValueOnce(createMockProductRow()); await linkUpcToProduct(1, '012345678905', mockLogger); expect(upcRepo.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', mockLogger); }); it('should throw error for invalid UPC code', async () => { await expect(linkUpcToProduct(1, 'invalid', mockLogger)).rejects.toThrow( 'Invalid UPC code format. UPC codes must be 8-14 digits.', ); }); it('should normalize UPC code before linking', async () => { vi.mocked(upcRepo.linkUpcToProduct).mockResolvedValueOnce(createMockProductRow()); await linkUpcToProduct(1, '012-345-678-905', mockLogger); expect(upcRepo.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', mockLogger); }); }); describe('getScanHistory', () => { it('should return paginated scan history', async () => { const mockHistory = { scans: [ { scan_id: 1, user_id: 'user-1', upc_code: '012345678905', product_id: 1, scan_source: 'manual_entry' as UpcScanSource, scan_confidence: 1.0, raw_image_path: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }, ], total: 1, }; vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce(mockHistory); const result = await getScanHistory({ user_id: 'user-1', limit: 10, offset: 0 }, mockLogger); expect(result.scans).toHaveLength(1); expect(result.total).toBe(1); }); it('should filter by scan source', async () => { vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce({ scans: [], total: 0 }); await getScanHistory({ user_id: 'user-1', scan_source: 'image_upload' }, mockLogger); expect(upcRepo.getScanHistory).toHaveBeenCalledWith( { user_id: 'user-1', scan_source: 'image_upload' }, mockLogger, ); }); it('should filter by date range', async () => { vi.mocked(upcRepo.getScanHistory).mockResolvedValueOnce({ scans: [], total: 0 }); await getScanHistory( { user_id: 'user-1', from_date: '2024-01-01', to_date: '2024-01-31', }, mockLogger, ); expect(upcRepo.getScanHistory).toHaveBeenCalledWith( { user_id: 'user-1', from_date: '2024-01-01', to_date: '2024-01-31', }, mockLogger, ); }); }); describe('getScanStats', () => { it('should return user scan statistics', async () => { const mockStats = { total_scans: 100, successful_lookups: 80, unique_products: 50, scans_today: 5, scans_this_week: 20, }; vi.mocked(upcRepo.getUserScanStats).mockResolvedValueOnce(mockStats); const result = await getScanStats('user-1', mockLogger); expect(result).toEqual(mockStats); expect(upcRepo.getUserScanStats).toHaveBeenCalledWith('user-1', mockLogger); }); }); describe('getScanById', () => { it('should return scan record by ID', async () => { const mockScan = { scan_id: 1, user_id: 'user-1', upc_code: '012345678905', product_id: 1, scan_source: 'manual_entry' as UpcScanSource, scan_confidence: 1.0, raw_image_path: null, lookup_successful: true, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; vi.mocked(upcRepo.getScanById).mockResolvedValueOnce(mockScan); const result = await getScanById(1, 'user-1', mockLogger); expect(result).toEqual(mockScan); expect(upcRepo.getScanById).toHaveBeenCalledWith(1, 'user-1', mockLogger); }); }); describe('lookupExternalUpc - additional coverage', () => { it('should use image_front_url as fallback when image_url is missing', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { product_name: 'Test Product', brands: 'Test Brand', image_url: null, image_front_url: 'https://example.com/front.jpg', }, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.image_url).toBe('https://example.com/front.jpg'); }); it('should return Unknown Product when both product_name and generic_name are missing', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { brands: 'Test Brand', // No product_name or generic_name }, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.name).toBe('Unknown Product'); }); it('should handle category without en: prefix', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 1, product: { product_name: 'Test Product', categories_tags: ['snacks'], // No en: prefix }, }), }); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.category).toBe('snacks'); }); it('should handle non-Error thrown in catch block', async () => { mockFetch.mockRejectedValueOnce('String error'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); }); describe('scanUpc - additional coverage', () => { it('should not set external_lookup when cached lookup was unsuccessful', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce({ lookup_id: 1, upc_code: '012345678905', product_name: null, brand_name: null, category: null, description: null, image_url: null, external_source: 'unknown', lookup_data: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 5, user_id: 'user-1', upc_code: '012345678905', product_id: null, scan_source: 'manual_entry', scan_confidence: 1.0, raw_image_path: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); const result = await scanUpc( 'user-1', { upc_code: '012345678905', scan_source: 'manual_entry' }, mockLogger, ); expect(result.external_lookup).toBeNull(); expect(result.lookup_successful).toBe(false); expect(mockFetch).not.toHaveBeenCalled(); }); it('should cache unsuccessful external lookup result', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); vi.mocked(upcRepo.recordScan).mockResolvedValueOnce({ scan_id: 6, user_id: 'user-1', upc_code: '012345678905', product_id: null, scan_source: 'manual_entry', scan_confidence: 1.0, raw_image_path: null, lookup_successful: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }); // External lookup returns nothing mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }); const result = await scanUpc( 'user-1', { upc_code: '012345678905', scan_source: 'manual_entry' }, mockLogger, ); expect(result.external_lookup).toBeNull(); expect(upcRepo.upsertExternalLookup).toHaveBeenCalledWith( '012345678905', 'unknown', false, expect.anything(), {}, ); }); }); describe('lookupUpc - additional coverage', () => { it('should cache unsuccessful external lookup and return found=false', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); // External lookup returns nothing mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }); const result = await lookupUpc({ upc_code: '012345678905' }, mockLogger); expect(result.found).toBe(false); expect(result.from_cache).toBe(false); expect(result.external_lookup).toBeNull(); }); it('should use custom max_cache_age_hours', async () => { vi.mocked(upcRepo.findProductByUpc).mockResolvedValueOnce(null); vi.mocked(upcRepo.findExternalLookup).mockResolvedValueOnce(null); vi.mocked(upcRepo.upsertExternalLookup).mockResolvedValueOnce( createMockExternalLookupRecord(), ); mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }); await lookupUpc({ upc_code: '012345678905', max_cache_age_hours: 24 }, mockLogger); expect(upcRepo.findExternalLookup).toHaveBeenCalledWith( '012345678905', 24, expect.anything(), ); }); }); }); /** * Tests for UPC Item DB and Barcode Lookup APIs when configured. * These require separate describe blocks to re-mock the config module. */ describe('upcService.server - with API keys configured', () => { let mockLogger: Logger; const mockFetch = vi.fn(); beforeEach(async () => { vi.clearAllMocks(); vi.resetModules(); global.fetch = mockFetch; mockFetch.mockReset(); // Re-mock with API keys configured vi.doMock('../config/env', () => ({ config: { upc: { upcItemDbApiKey: 'test-upcitemdb-key', barcodeLookupApiKey: 'test-barcodelookup-key', }, }, isUpcItemDbConfigured: true, isBarcodeLookupConfigured: true, })); vi.doMock('./db/index.db', () => ({ upcRepo: { recordScan: vi.fn(), findProductByUpc: vi.fn(), findExternalLookup: vi.fn(), upsertExternalLookup: vi.fn(), linkUpcToProduct: vi.fn(), getScanHistory: vi.fn(), getUserScanStats: vi.fn(), getScanById: vi.fn(), }, })); mockLogger = createMockLogger(); }); afterEach(() => { vi.resetAllMocks(); }); describe('lookupExternalUpc with UPC Item DB', () => { it('should return product from UPC Item DB when Open Food Facts has no result', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns product .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [ { title: 'UPC Item DB Product', brand: 'UPC Brand', category: 'Electronics', description: 'A test product', images: ['https://example.com/upcitemdb.jpg'], }, ], }), }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).not.toBeNull(); expect(result?.name).toBe('UPC Item DB Product'); expect(result?.brand).toBe('UPC Brand'); expect(result?.source).toBe('upcitemdb'); }); it('should handle UPC Item DB rate limit (429)', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB rate limit .mockResolvedValueOnce({ ok: false, status: 429, }) // Barcode Lookup also returns nothing .mockResolvedValueOnce({ ok: false, status: 404, }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); expect(mockLogger.warn).toHaveBeenCalledWith( { upcCode: '012345678905' }, 'UPC Item DB rate limit exceeded', ); }); it('should handle UPC Item DB network error', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB network error .mockRejectedValueOnce(new Error('Network error')) // Barcode Lookup also errors .mockRejectedValueOnce(new Error('Network error')); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should handle UPC Item DB empty items array', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns empty items .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup also returns nothing .mockResolvedValueOnce({ ok: false, status: 404, }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should return Unknown Product when UPC Item DB item has no title', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns item without title .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [{ brand: 'Some Brand' }], }), }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.name).toBe('Unknown Product'); expect(result?.source).toBe('upcitemdb'); }); }); describe('lookupExternalUpc with Barcode Lookup', () => { it('should return product from Barcode Lookup when other APIs have no result', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup returns product .mockResolvedValueOnce({ ok: true, json: async () => ({ products: [ { title: 'Barcode Lookup Product', brand: 'BL Brand', category: 'Food', description: 'A barcode lookup product', images: ['https://example.com/barcodelookup.jpg'], }, ], }), }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).not.toBeNull(); expect(result?.name).toBe('Barcode Lookup Product'); expect(result?.source).toBe('barcodelookup'); }); it('should handle Barcode Lookup rate limit (429)', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup rate limit .mockResolvedValueOnce({ ok: false, status: 429, }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); expect(mockLogger.warn).toHaveBeenCalledWith( { upcCode: '012345678905' }, 'Barcode Lookup rate limit exceeded', ); }); it('should handle Barcode Lookup 404 response', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup 404 .mockResolvedValueOnce({ ok: false, status: 404, }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should use product_name fallback when title is missing in Barcode Lookup', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup with product_name instead of title .mockResolvedValueOnce({ ok: true, json: async () => ({ products: [ { product_name: 'Product Name Fallback', brand: 'BL Brand', }, ], }), }); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result?.name).toBe('Product Name Fallback'); }); it('should handle Barcode Lookup network error', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup network error .mockRejectedValueOnce(new Error('Network error')); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); it('should handle non-Error thrown in Barcode Lookup', async () => { // Open Food Facts returns nothing mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ status: 0, product: null }), }) // UPC Item DB returns nothing .mockResolvedValueOnce({ ok: true, json: async () => ({ code: 'OK', items: [] }), }) // Barcode Lookup throws non-Error .mockRejectedValueOnce('String error thrown'); const { lookupExternalUpc } = await import('./upcService.server'); const result = await lookupExternalUpc('012345678905', mockLogger); expect(result).toBeNull(); }); }); });