import { readFile, readdir } from 'node:fs/promises'; import { Readable } from 'node:stream'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { saveFile } from '../src/lib/storage.js'; import { PayloadTooLargeError } from '../src/errors.js'; import { makeTempDir, removeDir, TINY_PNG } from './helpers.js'; let uploadDir: string; beforeEach(async () => { uploadDir = await makeTempDir(); }); afterEach(async () => { await removeDir(uploadDir); }); function params(maxFileSizeBytes = 1024 * 1024): { targetDir: string; maxFileSizeBytes: number } { return { targetDir: uploadDir, maxFileSizeBytes }; } describe('saveFile', () => { it('écrit le fichier et renvoie ses métadonnées', async () => { const stored = await saveFile({ source: Readable.from(TINY_PNG), originalName: 'photo.png', mimeType: 'image/png', ...params(), }); expect(stored.size).toBe(TINY_PNG.length); expect(stored.storedName).toMatch(/^photo-\d+-[0-9a-f]{8}\.png$/); expect(await readFile(stored.path)).toEqual(TINY_PNG); }); it('produit un nom sans séparateur de chemin même pour un nom hostile', async () => { const stored = await saveFile({ source: Readable.from(TINY_PNG), originalName: '../../etc/passwd', mimeType: 'image/png', ...params(), }); expect(stored.storedName).not.toMatch(/[/\\]/); expect(stored.storedName).not.toContain('..'); expect(stored.storedName).toMatch(/^passwd-\d+-[0-9a-f]{8}\.png$/); }); it('assainit les noms avec espaces et accents', async () => { const stored = await saveFile({ source: Readable.from(TINY_PNG), originalName: 'mon joli cliché.PNG', mimeType: 'image/png', ...params(), }); expect(stored.storedName).toMatch(/^[A-Za-z0-9_-]+-\d+-[0-9a-f]{8}\.png$/); }); it('rejette au-delà de la taille maximale sans laisser de fichier partiel', async () => { await expect( saveFile({ source: Readable.from(Buffer.alloc(5000, 1)), originalName: 'big.png', mimeType: 'image/png', ...params(1000), }), ).rejects.toBeInstanceOf(PayloadTooLargeError); expect(await readdir(uploadDir)).toHaveLength(0); }); });