logging + e2e test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user