import { describe, expect, it, vi } from 'vitest' import { detectImageMime, prepareImageForUpload, validateImageFile } from '@/lib/fileValidation' const PNG_HEADER = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) const JPEG_HEADER = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46]) function blobPart(bytes: Uint8Array): ArrayBuffer { const buffer = new ArrayBuffer(bytes.byteLength) new Uint8Array(buffer).set(bytes) return buffer } /** Minimal HEIC-like ftyp box: size(4) + 'ftyp' + 'heic' */ function heicHeader(): Uint8Array { const bytes = new Uint8Array(16) bytes.set([0x00, 0x00, 0x00, 0x18], 0) bytes.set(new TextEncoder().encode('ftypheic'), 4) return bytes } describe('detectImageMime', () => { it('detects JPEG/PNG from magic bytes even when declared type is empty', () => { expect(detectImageMime(JPEG_HEADER, '')).toBe('image/jpeg') expect(detectImageMime(PNG_HEADER, '')).toBe('image/png') }) it('detects HEIC from ftyp brand or declared mime', () => { expect(detectImageMime(heicHeader(), '')).toBe('image/heic') expect(detectImageMime(new Uint8Array(16), 'image/heic')).toBe('image/heic') }) }) describe('image upload validation', () => { it('accepts a PNG with a valid binary signature', async () => { const file = new File([blobPart(PNG_HEADER)], 'safe.png', { type: 'image/png' }) await expect(validateImageFile(file)).resolves.toBeNull() }) it('accepts JPEG when file.type is empty (common on iOS Safari)', async () => { const file = new File([blobPart(JPEG_HEADER)], 'photo.jpg', { type: '' }) const prepared = await prepareImageForUpload(file) expect(typeof prepared === 'string' ? prepared : prepared.type).toBe('image/jpeg') await expect(validateImageFile(file)).resolves.toBeNull() }) it('rejects spoofed MIME, SVG and oversized images', async () => { const spoofed = new File([''], 'attack.png', { type: 'image/png' }) const svg = new File([''], 'attack.svg', { type: 'image/svg+xml' }) const oversized = new File([new Uint8Array(17)], 'large.jpg', { type: 'image/jpeg' }) await expect(validateImageFile(spoofed)).resolves.toMatch(/مجاز هستند/) await expect(validateImageFile(svg)).resolves.toMatch(/مجاز هستند/) await expect(validateImageFile(oversized, 0.000001)).resolves.toMatch(/حجم تصویر/) }) it('rejects GIF, since the backend only accepts JPEG/PNG/WebP (and HEIC, which it converts server-side)', async () => { const gif = new File([new TextEncoder().encode('GIF89a')], 'attack.gif', { type: 'image/gif' }) await expect(validateImageFile(gif)).resolves.toMatch(/مجاز هستند/) }) it('converts HEIC to JPEG when createImageBitmap succeeds', async () => { const close = vi.fn() const drawImage = vi.fn() vi.stubGlobal( 'createImageBitmap', vi.fn(async () => ({ width: 2, height: 2, close, })) ) const getContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({ drawImage, } as unknown as CanvasRenderingContext2D) vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation(function toBlob(callback) { callback?.(new Blob([JPEG_HEADER], { type: 'image/jpeg' })) }) const file = new File([blobPart(heicHeader())], 'id.heic', { type: 'image/heic' }) const prepared = await prepareImageForUpload(file) expect(prepared).toBeInstanceOf(File) expect((prepared as File).type).toBe('image/jpeg') expect((prepared as File).name).toBe('id.jpg') expect(close).toHaveBeenCalled() getContext.mockRestore() vi.unstubAllGlobals() vi.restoreAllMocks() }) it('falls back to the original HEIC file when the browser cannot decode it (e.g. Chrome/Edge on Windows), letting the backend convert it', async () => { vi.stubGlobal( 'createImageBitmap', vi.fn(async () => { throw new Error('unsupported') }) ) const file = new File([blobPart(heicHeader())], 'id.heic', { type: 'image/heic' }) const prepared = await prepareImageForUpload(file) expect(prepared).toBeInstanceOf(File) expect((prepared as File).type).toBe('image/heic') expect((prepared as File).name).toBe('id.heic') vi.unstubAllGlobals() }) it('relabels a HEIC file with an empty declared type without corrupting its extension', async () => { vi.stubGlobal( 'createImageBitmap', vi.fn(async () => { throw new Error('unsupported') }) ) const file = new File([blobPart(heicHeader())], 'id.heic', { type: '' }) const prepared = await prepareImageForUpload(file) expect(prepared).toBeInstanceOf(File) expect((prepared as File).type).toBe('image/heic') expect((prepared as File).name).toBe('id.heic') vi.unstubAllGlobals() }) })