Files
flyer-crawler.projectium.com/src/services/barcodeService.server.test.ts
Torben Sorensen 2a5cc5bb51
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
unit test repairs
2026-01-12 08:10:37 -08:00

409 lines
14 KiB
TypeScript

// src/services/barcodeService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Logger } from 'pino';
import type { Job } from 'bullmq';
import type { BarcodeDetectionJobData } from '../types/job-data';
import { createMockLogger } from '../tests/utils/mockLogger';
// Unmock the barcodeService module so we can test the real implementation
// The global test setup mocks this to prevent zxing-wasm issues, but we need the real module here
vi.unmock('./barcodeService.server');
// Mock dependencies
vi.mock('zxing-wasm/reader', () => ({
readBarcodesFromImageData: vi.fn(),
}));
vi.mock('sharp', () => {
const mockSharp = vi.fn(() => ({
metadata: vi.fn().mockResolvedValue({ width: 100, height: 100 }),
ensureAlpha: vi.fn().mockReturnThis(),
raw: vi.fn().mockReturnThis(),
toBuffer: vi.fn().mockResolvedValue({
data: new Uint8Array(100 * 100 * 4),
info: { width: 100, height: 100 },
}),
grayscale: vi.fn().mockReturnThis(),
normalize: vi.fn().mockReturnThis(),
sharpen: vi.fn().mockReturnThis(),
toFile: vi.fn().mockResolvedValue(undefined),
}));
return { default: mockSharp };
});
vi.mock('node:fs/promises', () => ({
default: {
readFile: vi.fn().mockResolvedValue(Buffer.from('mock image data')),
},
}));
vi.mock('./db/index.db', () => ({
upcRepo: {
updateScanWithDetectedCode: vi.fn().mockResolvedValue(undefined),
},
}));
// Import after mocks are set up
import {
detectBarcode,
isValidUpcFormat,
calculateUpcCheckDigit,
validateUpcCheckDigit,
processBarcodeDetectionJob,
detectMultipleBarcodes,
enhanceImageForDetection,
} from './barcodeService.server';
describe('barcodeService.server', () => {
let mockLogger: Logger;
beforeEach(() => {
vi.clearAllMocks();
mockLogger = createMockLogger();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('detectBarcode', () => {
it('should detect a valid UPC-A barcode from image', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '012345678905', format: 'UPC-A' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.upc_code).toBe('012345678905');
expect(result.format).toBe('UPC-A');
expect(result.confidence).toBe(0.95);
expect(result.error).toBeNull();
});
it('should detect a valid UPC-E barcode from image', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '01234567', format: 'UPC-E' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.upc_code).toBe('01234567');
expect(result.format).toBe('UPC-E');
});
it('should detect a valid EAN-13 barcode from image', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '5901234123457', format: 'EAN-13' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.upc_code).toBe('5901234123457');
expect(result.format).toBe('EAN-13');
});
it('should detect a valid EAN-8 barcode from image', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '96385074', format: 'EAN-8' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.upc_code).toBe('96385074');
expect(result.format).toBe('EAN-8');
});
it('should return detected: false when no barcode found', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(false);
expect(result.upc_code).toBeNull();
expect(result.confidence).toBeNull();
expect(result.format).toBeNull();
expect(result.error).toBeNull();
});
it('should return error when image dimensions cannot be determined', async () => {
const sharp = (await import('sharp')).default;
vi.mocked(sharp).mockReturnValueOnce({
metadata: vi.fn().mockResolvedValue({}),
ensureAlpha: vi.fn().mockReturnThis(),
raw: vi.fn().mockReturnThis(),
toBuffer: vi.fn(),
} as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(false);
expect(result.error).toBe('Could not determine image dimensions');
});
it('should handle errors during detection gracefully', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(new Error('Detection failed'));
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(false);
expect(result.error).toBe('Detection failed');
});
it('should map unknown barcode formats to "unknown"', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '12345678', format: 'SomeFutureFormat' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.format).toBe('unknown');
});
it('should calculate lower confidence when text is empty', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '', format: 'UPC-A' },
] as any);
const result = await detectBarcode('/path/to/image.jpg', mockLogger);
expect(result.detected).toBe(true);
expect(result.confidence).toBe(0.5);
});
});
describe('isValidUpcFormat', () => {
it('should return true for valid 12-digit UPC-A', () => {
expect(isValidUpcFormat('012345678905')).toBe(true);
});
it('should return true for valid 8-digit UPC-E', () => {
expect(isValidUpcFormat('01234567')).toBe(true);
});
it('should return true for valid 13-digit EAN-13', () => {
expect(isValidUpcFormat('5901234123457')).toBe(true);
});
it('should return true for valid 8-digit EAN-8', () => {
expect(isValidUpcFormat('96385074')).toBe(true);
});
it('should return true for valid 14-digit GTIN-14', () => {
expect(isValidUpcFormat('00012345678905')).toBe(true);
});
it('should return false for code with less than 8 digits', () => {
expect(isValidUpcFormat('1234567')).toBe(false);
});
it('should return false for code with more than 14 digits', () => {
expect(isValidUpcFormat('123456789012345')).toBe(false);
});
it('should return false for code with non-numeric characters', () => {
expect(isValidUpcFormat('01234567890A')).toBe(false);
});
it('should return false for empty string', () => {
expect(isValidUpcFormat('')).toBe(false);
});
});
describe('calculateUpcCheckDigit', () => {
it('should calculate correct check digit for valid 11-digit code', () => {
// UPC-A: 01234567890 has check digit 5
expect(calculateUpcCheckDigit('01234567890')).toBe(5);
});
it('should return null for code with wrong length', () => {
expect(calculateUpcCheckDigit('1234567890')).toBeNull(); // 10 digits
expect(calculateUpcCheckDigit('123456789012')).toBeNull(); // 12 digits
});
it('should return null for code with non-numeric characters', () => {
expect(calculateUpcCheckDigit('0123456789A')).toBeNull();
});
it('should handle all zeros', () => {
// 00000000000 should produce a valid check digit
const checkDigit = calculateUpcCheckDigit('00000000000');
expect(typeof checkDigit).toBe('number');
expect(checkDigit).toBeGreaterThanOrEqual(0);
expect(checkDigit).toBeLessThanOrEqual(9);
});
});
describe('validateUpcCheckDigit', () => {
it('should return true for valid UPC-A with correct check digit', () => {
expect(validateUpcCheckDigit('012345678905')).toBe(true);
});
it('should return false for UPC-A with incorrect check digit', () => {
expect(validateUpcCheckDigit('012345678901')).toBe(false);
});
it('should return false for code with wrong length', () => {
expect(validateUpcCheckDigit('01234567890')).toBe(false); // 11 digits
expect(validateUpcCheckDigit('0123456789012')).toBe(false); // 13 digits
});
it('should return false for code with non-numeric characters', () => {
expect(validateUpcCheckDigit('01234567890A')).toBe(false);
});
});
describe('processBarcodeDetectionJob', () => {
it('should process job and update scan record when barcode detected', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
const { upcRepo } = await import('./db/index.db');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '012345678905', format: 'UPC-A' },
] as any);
const mockJob = {
id: 'job-1',
data: {
scanId: 123,
imagePath: '/path/to/barcode.jpg',
userId: 'user-1',
meta: { requestId: 'req-1' },
},
} as Job<BarcodeDetectionJobData>;
const result = await processBarcodeDetectionJob(mockJob, mockLogger);
expect(result.detected).toBe(true);
expect(result.upc_code).toBe('012345678905');
expect(upcRepo.updateScanWithDetectedCode).toHaveBeenCalledWith(
123,
'012345678905',
0.95,
expect.any(Object),
);
});
it('should not update scan record when no barcode detected', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
const { upcRepo } = await import('./db/index.db');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);
const mockJob = {
id: 'job-2',
data: {
scanId: 456,
imagePath: '/path/to/no-barcode.jpg',
userId: 'user-2',
},
} as Job<BarcodeDetectionJobData>;
const result = await processBarcodeDetectionJob(mockJob, mockLogger);
expect(result.detected).toBe(false);
expect(upcRepo.updateScanWithDetectedCode).not.toHaveBeenCalled();
});
it('should return error result when job processing fails', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(new Error('Processing error'));
const mockJob = {
id: 'job-3',
data: {
scanId: 789,
imagePath: '/path/to/error.jpg',
userId: 'user-3',
},
} as Job<BarcodeDetectionJobData>;
const result = await processBarcodeDetectionJob(mockJob, mockLogger);
expect(result.detected).toBe(false);
expect(result.error).toBe('Processing error');
});
});
describe('detectMultipleBarcodes', () => {
it('should detect multiple barcodes in an image', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([
{ text: '012345678905', format: 'UPC-A' },
{ text: '5901234123457', format: 'EAN-13' },
{ text: '96385074', format: 'EAN-8' },
] as any);
const results = await detectMultipleBarcodes('/path/to/multi.jpg', mockLogger);
expect(results).toHaveLength(3);
expect(results[0].upc_code).toBe('012345678905');
expect(results[0].format).toBe('UPC-A');
expect(results[1].upc_code).toBe('5901234123457');
expect(results[1].format).toBe('EAN-13');
expect(results[2].upc_code).toBe('96385074');
expect(results[2].format).toBe('EAN-8');
});
it('should return empty array when no barcodes detected', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockResolvedValueOnce([]);
const results = await detectMultipleBarcodes('/path/to/no-codes.jpg', mockLogger);
expect(results).toEqual([]);
});
it('should return empty array on error', async () => {
const { readBarcodesFromImageData } = await import('zxing-wasm/reader');
vi.mocked(readBarcodesFromImageData).mockRejectedValueOnce(
new Error('Multi-detection failed'),
);
const results = await detectMultipleBarcodes('/path/to/error.jpg', mockLogger);
expect(results).toEqual([]);
});
});
describe('enhanceImageForDetection', () => {
it('should enhance image and return new path', async () => {
const result = await enhanceImageForDetection('/path/to/image.jpg', mockLogger);
expect(result).toBe('/path/to/image-enhanced.jpg');
});
it('should handle different file extensions', async () => {
const result = await enhanceImageForDetection('/path/to/image.png', mockLogger);
expect(result).toBe('/path/to/image-enhanced.png');
});
it('should return original path on enhancement failure', async () => {
const sharp = (await import('sharp')).default;
vi.mocked(sharp).mockReturnValueOnce({
grayscale: vi.fn().mockReturnThis(),
normalize: vi.fn().mockReturnThis(),
sharpen: vi.fn().mockReturnThis(),
toFile: vi.fn().mockRejectedValue(new Error('Enhancement failed')),
} as any);
const result = await enhanceImageForDetection('/path/to/image.jpg', mockLogger);
expect(result).toBe('/path/to/image.jpg');
});
});
});