Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
409 lines
14 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|