import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { mkdtemp, mkdir, stat, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import type { APIRoute } from 'astro'; let workDir: string; let POST: APIRoute; async function setupTmpRoot() { workDir = await mkdtemp(join(tmpdir(), 'portfolio-upload-')); await mkdir(join(workDir, 'data', 'uploads'), { recursive: true }); vi.spyOn(process, 'cwd').mockReturnValue(workDir); vi.resetModules(); const mod = await import('../../src/pages/api/upload'); POST = mod.POST as APIRoute; } async function cleanup() { vi.restoreAllMocks(); if (workDir) await rm(workDir, { recursive: true, force: true }); } beforeEach(setupTmpRoot); afterEach(cleanup); function call(form: FormData) { const request = new Request('http://localhost/api/upload', { method: 'POST', body: form }); return POST({ request } as unknown as Parameters[0]) as Promise; } describe('POST /api/upload', () => { it('400 no_file quand aucun fichier', async () => { const res = await call(new FormData()); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: 'no_file' }); }); it('400 invalid_extension sur .exe', async () => { const form = new FormData(); form.append('file', new File(['x'], 'malware.exe', { type: 'application/octet-stream' })); const res = await call(form); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: 'invalid_extension' }); }); it('400 invalid_extension sur .svg (retiré pour anti-XSS)', async () => { const form = new FormData(); form.append('file', new File([''], 'evil.svg', { type: 'image/svg+xml' })); const res = await call(form); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: 'invalid_extension' }); }); it('400 file_too_large quand image > 5 MB', async () => { const form = new FormData(); const bigBuffer = new Uint8Array(5 * 1024 * 1024 + 1); form.append('file', new File([bigBuffer], 'big.png', { type: 'image/png' })); const res = await call(form); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: 'file_too_large' }); }); it('200 ok sur une image PNG valide', async () => { const form = new FormData(); form.append('file', new File([new Uint8Array(1024)], 'photo.png', { type: 'image/png' })); const res = await call(form); expect(res.status).toBe(200); const body = await res.json(); expect(body.ok).toBe(true); expect(body.url).toBe('/api/files/img/photo.png'); const written = await stat(join(workDir, 'data', 'uploads', 'img', 'photo.png')); expect(written.size).toBe(1024); }); it('200 ok sur un PDF avec kind=document → bucket assets', async () => { const form = new FormData(); form.append('file', new File([new Uint8Array(2048)], 'cv.pdf', { type: 'application/pdf' })); form.append('kind', 'document'); const res = await call(form); expect(res.status).toBe(200); const body = await res.json(); expect(body.url).toBe('/api/files/assets/cv.pdf'); const written = await stat(join(workDir, 'data', 'uploads', 'assets', 'cv.pdf')); expect(written.size).toBe(2048); }); it('sanitize folder : "evil/../etc" → "eviletc" (caractères non alphanum retirés)', async () => { const form = new FormData(); form.append('file', new File([new Uint8Array(10)], 'a.png', { type: 'image/png' })); form.append('folder', 'evil/../etc'); const res = await call(form); expect(res.status).toBe(200); const body = await res.json(); expect(body.url).toBe('/api/files/img/eviletc/a.png'); const written = await stat(join(workDir, 'data', 'uploads', 'img', 'eviletc', 'a.png')); expect(written.size).toBe(10); }); });