import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockMkdir, mockWriteFile, mockUnlink } = vi.hoisted(() => ({ mockMkdir: vi.fn().mockResolvedValue(undefined), mockWriteFile: vi.fn().mockResolvedValue(undefined), mockUnlink: vi.fn().mockResolvedValue(undefined), })) vi.mock('fs/promises', () => ({ __esModule: true, default: { mkdir: mockMkdir, writeFile: mockWriteFile, unlink: mockUnlink }, mkdir: mockMkdir, writeFile: mockWriteFile, unlink: mockUnlink, })) import { saveImage, validateImageMagicBytes, deleteImageFile } from '@/lib/storage' function makeJpegBuffer(): Buffer { const b = Buffer.alloc(12) b[0] = 0xff; b[1] = 0xd8; b[2] = 0xff return b } function makePngBuffer(): Buffer { const b = Buffer.alloc(12) b[0] = 0x89; b[1] = 0x50; b[2] = 0x4e; b[3] = 0x47 return b } function makeWebpBuffer(): Buffer { const b = Buffer.alloc(12) b[0] = 0x52; b[1] = 0x49; b[2] = 0x46; b[3] = 0x46 b[8] = 0x57; b[9] = 0x45; b[10] = 0x42; b[11] = 0x50 return b } function makeIcoBuffer(): Buffer { const b = Buffer.alloc(12) b[0] = 0x00; b[1] = 0x00; b[2] = 0x01; b[3] = 0x00 return b } function makeFileFromBuffer(buf: Buffer, type: string): File { return new File([new Uint8Array(buf)], 'test.img', { type }) } beforeEach(() => { vi.clearAllMocks() mockMkdir.mockResolvedValue(undefined) mockWriteFile.mockResolvedValue(undefined) mockUnlink.mockResolvedValue(undefined) }) // ─── validateImageMagicBytes ────────────────────────────────────────────────── describe('validateImageMagicBytes', () => { it('accepts a valid JPEG', async () => { const file = makeFileFromBuffer(makeJpegBuffer(), 'image/jpeg') expect(await validateImageMagicBytes(file, 'image/jpeg')).toBe(true) }) it('accepts a valid PNG', async () => { const file = makeFileFromBuffer(makePngBuffer(), 'image/png') expect(await validateImageMagicBytes(file, 'image/png')).toBe(true) }) it('accepts a valid WebP', async () => { const file = makeFileFromBuffer(makeWebpBuffer(), 'image/webp') expect(await validateImageMagicBytes(file, 'image/webp')).toBe(true) }) it('accepts a valid ICO', async () => { const file = makeFileFromBuffer(makeIcoBuffer(), 'image/x-icon') expect(await validateImageMagicBytes(file, 'image/x-icon')).toBe(true) }) it('rejects JPEG buffer declared as PNG', async () => { const file = makeFileFromBuffer(makeJpegBuffer(), 'image/png') expect(await validateImageMagicBytes(file, 'image/png')).toBe(false) }) it('rejects unknown MIME type', async () => { const file = makeFileFromBuffer(makeJpegBuffer(), 'image/bmp') expect(await validateImageMagicBytes(file, 'image/bmp')).toBe(false) }) it('rejects a random buffer as JPEG', async () => { const file = makeFileFromBuffer(Buffer.from([0x00, 0x01, 0x02]), 'image/jpeg') expect(await validateImageMagicBytes(file, 'image/jpeg')).toBe(false) }) }) // ─── saveImage ──────────────────────────────────────────────────────────────── describe('saveImage', () => { it('creates the product directory and writes file', async () => { const buf = Buffer.from('fakeimage') const url = await saveImage('prod-123', buf, 'photo.jpg') expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('prod-123'), { recursive: true }) expect(mockWriteFile).toHaveBeenCalled() expect(url).toMatch(/^\/uploads\/prod-123\//) expect(url).toMatch(/photo\.jpg$/) }) it('sanitizes filename (removes special chars)', async () => { const buf = Buffer.from('fakeimage') const url = await saveImage('prod-123', buf, 'my photo (1).JPG') expect(url).toMatch(/my_photo__1_\.jpg$/) }) it('returns a URL starting with /uploads/', async () => { const url = await saveImage('prod-abc', Buffer.from('x'), 'img.png') expect(url).toMatch(/^\/uploads\/prod-abc\//) }) }) // ─── deleteImageFile ────────────────────────────────────────────────────────── describe('deleteImageFile', () => { it('calls unlink with the correct path', async () => { await deleteImageFile('/uploads/prod-1/image.jpg') expect(mockUnlink).toHaveBeenCalledWith(expect.stringContaining('/uploads/prod-1/image.jpg')) }) })