mediaDrop/tests/storage.test.ts
Nicolas fe8bd26bb7 Initial commit: mediaDrop — serveur de dépôt de fichiers médias
Serveur TypeScript (Fastify 5) permettant de déposer des fichiers médias
dans des dossiers nommés, via une page web de glisser-déposer.

- Authentification OpenID Connect gérée côté serveur (Authorization Code +
  PKCE, client confidentiel, openid-client) ; session via cookie chiffré
  (@fastify/secure-session).
- Configuration dans un fichier JSON (schéma fourni) ; l'environnement ne
  porte que le pointeur MEDIADROP_CONFIG.
- Cibles de dépôt nommées : liste { name, label, dir }, sélection par
  ?target=, première cible par défaut.
- Upload multipart en streaming : nom de fichier dérivé côté serveur (pas
  de path traversal), liste blanche de types MIME, plafond de taille.
- TypeScript strict, ESLint + Prettier sans warning, 46 tests Vitest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 23:06:53 +02:00

73 lines
2.2 KiB
TypeScript

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);
});
});