logging + e2e test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s

This commit is contained in:
2026-01-12 19:09:56 -08:00
parent 62a8a8bf4b
commit 6e36cc3b07
13 changed files with 1859 additions and 56 deletions

View File

@@ -671,4 +671,531 @@ describe('upcService.server', () => {
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();
});
});
});