// src/services/flyerFileHandler.server.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Job } from 'bullmq'; import type { Dirent } from 'node:fs'; import sharp from 'sharp'; import { FlyerFileHandler, ICommandExecutor, IFileSystem } from './flyerFileHandler.server'; import { ImageConversionError, PdfConversionError, UnsupportedFileTypeError } from './processingErrors'; import { logger } from './logger.server'; import type { FlyerJobData } from '../types/job-data'; // Mock dependencies vi.mock('sharp', () => { const mockSharpInstance = { jpeg: vi.fn().mockReturnThis(), png: vi.fn().mockReturnThis(), toFile: vi.fn().mockResolvedValue({}), }; return { __esModule: true, default: vi.fn(() => mockSharpInstance), }; }); vi.mock('./logger.server', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), child: vi.fn().mockReturnThis(), }, })); const createMockJob = (data: Partial): Job => { return { id: 'job-1', data: { filePath: '/tmp/flyer.jpg', originalFileName: 'flyer.jpg', checksum: 'checksum-123', ...data, }, updateProgress: vi.fn(), } as unknown as Job; }; describe('FlyerFileHandler', () => { let service: FlyerFileHandler; let mockFs: IFileSystem; let mockExec: ICommandExecutor; beforeEach(() => { vi.clearAllMocks(); mockFs = { readdir: vi.fn().mockResolvedValue([]), unlink: vi.fn(), }; mockExec = vi.fn().mockResolvedValue({ stdout: 'success', stderr: '' }); service = new FlyerFileHandler(mockFs, mockExec); }); it('should convert a PDF and return image paths', async () => { const job = createMockJob({ filePath: '/tmp/flyer.pdf' }); vi.mocked(mockFs.readdir).mockResolvedValue([ { name: 'flyer-1.jpg' }, { name: 'flyer-2.jpg' }, ] as Dirent[]); const { imagePaths, createdImagePaths } = await service.prepareImageInputs( '/tmp/flyer.pdf', job, logger, ); expect(mockExec).toHaveBeenCalledWith('pdftocairo -jpeg -r 150 "/tmp/flyer.pdf" "/tmp/flyer"'); expect(imagePaths).toHaveLength(2); expect(imagePaths[0].path).toContain('flyer-1.jpg'); expect(createdImagePaths).toHaveLength(2); }); it('should throw PdfConversionError if PDF conversion yields no images', async () => { const job = createMockJob({ filePath: '/tmp/flyer.pdf' }); vi.mocked(mockFs.readdir).mockResolvedValue([]); // No images found await expect(service.prepareImageInputs('/tmp/flyer.pdf', job, logger)).rejects.toThrow( PdfConversionError, ); }); it('should convert convertible image types to PNG', async () => { const job = createMockJob({ filePath: '/tmp/flyer.gif' }); const mockSharpInstance = sharp('/tmp/flyer.gif'); vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any); const { imagePaths, createdImagePaths } = await service.prepareImageInputs( '/tmp/flyer.gif', job, logger, ); expect(sharp).toHaveBeenCalledWith('/tmp/flyer.gif'); expect(mockSharpInstance.png).toHaveBeenCalled(); expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-converted.png'); expect(imagePaths).toEqual([{ path: '/tmp/flyer-converted.png', mimetype: 'image/png' }]); expect(createdImagePaths).toEqual(['/tmp/flyer-converted.png']); }); it('should throw UnsupportedFileTypeError for unsupported types', async () => { const job = createMockJob({ filePath: '/tmp/document.txt' }); await expect(service.prepareImageInputs('/tmp/document.txt', job, logger)).rejects.toThrow( UnsupportedFileTypeError, ); }); describe('Image Processing', () => { it('should process a JPEG to strip EXIF data', async () => { const job = createMockJob({ filePath: '/tmp/flyer.jpg' }); const mockSharpInstance = sharp('/tmp/flyer.jpg'); vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any); const { imagePaths, createdImagePaths } = await service.prepareImageInputs( '/tmp/flyer.jpg', job, logger, ); expect(sharp).toHaveBeenCalledWith('/tmp/flyer.jpg'); expect(mockSharpInstance.jpeg).toHaveBeenCalledWith({ quality: 90 }); expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-processed.jpeg'); expect(imagePaths).toEqual([{ path: '/tmp/flyer-processed.jpeg', mimetype: 'image/jpeg' }]); expect(createdImagePaths).toEqual(['/tmp/flyer-processed.jpeg']); }); it('should process a PNG to strip metadata', async () => { const job = createMockJob({ filePath: '/tmp/flyer.png' }); const mockSharpInstance = sharp('/tmp/flyer.png'); vi.mocked(mockSharpInstance.toFile).mockResolvedValue({} as any); const { imagePaths, createdImagePaths } = await service.prepareImageInputs( '/tmp/flyer.png', job, logger, ); expect(sharp).toHaveBeenCalledWith('/tmp/flyer.png'); expect(mockSharpInstance.png).toHaveBeenCalledWith({ quality: 90 }); expect(mockSharpInstance.toFile).toHaveBeenCalledWith('/tmp/flyer-processed.png'); expect(imagePaths).toEqual([{ path: '/tmp/flyer-processed.png', mimetype: 'image/png' }]); expect(createdImagePaths).toEqual(['/tmp/flyer-processed.png']); }); it('should handle other supported image types (e.g. webp) directly without processing', async () => { const job = createMockJob({ filePath: '/tmp/flyer.webp' }); const { imagePaths, createdImagePaths } = await service.prepareImageInputs( '/tmp/flyer.webp', job, logger, ); expect(imagePaths).toEqual([{ path: '/tmp/flyer.webp', mimetype: 'image/webp' }]); expect(createdImagePaths).toEqual([]); expect(sharp).not.toHaveBeenCalled(); }); it('should throw ImageConversionError if sharp fails during JPEG processing', async () => { const job = createMockJob({ filePath: '/tmp/flyer.jpg' }); const sharpError = new Error('Sharp failed'); const mockSharpInstance = sharp('/tmp/flyer.jpg'); vi.mocked(mockSharpInstance.toFile).mockRejectedValue(sharpError); await expect(service.prepareImageInputs('/tmp/flyer.jpg', job, logger)).rejects.toThrow(ImageConversionError); }); it('should throw ImageConversionError if sharp fails during PNG processing', async () => { const job = createMockJob({ filePath: '/tmp/flyer.png' }); const sharpError = new Error('Sharp failed'); const mockSharpInstance = sharp('/tmp/flyer.png'); vi.mocked(mockSharpInstance.toFile).mockRejectedValue(sharpError); await expect(service.prepareImageInputs('/tmp/flyer.png', job, logger)).rejects.toThrow(ImageConversionError); }); }); });