mediaDrop/tests/auth-flow.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

98 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { FastifyInstance } from 'fastify';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { buildApp } from '../src/app.js';
import {
login,
makeConfig,
makeTempDir,
oneTarget,
removeDir,
SESSION_COOKIE,
startStubIdp,
type StubIdp,
} from './helpers.js';
let idp: StubIdp;
let app: FastifyInstance;
let uploadDir: string;
beforeAll(async () => {
idp = await startStubIdp();
});
afterAll(async () => {
await idp.close();
});
beforeEach(async () => {
uploadDir = await makeTempDir();
app = await buildApp(
makeConfig({
targets: oneTarget(uploadDir),
oidc: {
issuer: idp.issuer,
clientId: idp.clientId,
clientSecret: idp.clientSecret,
scopes: ['openid', 'profile', 'email'],
},
}),
{ logger: false },
);
await app.ready();
});
afterEach(async () => {
await app.close();
await removeDir(uploadDir);
});
describe('flux d’authentification', () => {
it('GET /auth/login redirige vers l’issuer avec un défi PKCE et pose un cookie', async () => {
const res = await app.inject({ method: 'GET', url: '/auth/login' });
expect(res.statusCode).toBe(302);
const target = new URL(res.headers.location as string);
expect(target.origin).toBe(idp.issuer);
expect(target.searchParams.get('code_challenge')).toBeTruthy();
expect(target.searchParams.get('state')).toBeTruthy();
expect(res.cookies.some((c) => c.name === SESSION_COOKIE)).toBe(true);
});
it('GET /auth/me est anonyme par défaut, authentifié après login', async () => {
const anon = await app.inject({ method: 'GET', url: '/auth/me' });
expect(anon.json()).toEqual({ authenticated: false });
const cookie = await login(app, idp, { name: 'Alice' });
const me = await app.inject({
method: 'GET',
url: '/auth/me',
cookies: { [SESSION_COOKIE]: cookie },
});
expect(me.json()).toEqual({ authenticated: true, name: 'Alice' });
});
it('POST /auth/logout vide la session', async () => {
const cookie = await login(app, idp);
const out = await app.inject({
method: 'POST',
url: '/auth/logout',
cookies: { [SESSION_COOKIE]: cookie },
});
expect(out.statusCode).toBe(302);
const cleared = out.cookies.find((c) => c.name === SESSION_COOKIE);
const afterCookie = cleared ? cleared.value : cookie;
const me = await app.inject({
method: 'GET',
url: '/auth/me',
cookies: { [SESSION_COOKIE]: afterCookie },
});
expect(me.json()).toEqual({ authenticated: false });
});
it('GET /auth/callback sans transaction redirige avec ?login=expired', async () => {
const res = await app.inject({ method: 'GET', url: '/auth/callback?code=x&state=y' });
expect(res.statusCode).toBe(302);
expect(res.headers.location).toBe('http://localhost:3000/?login=expired');
});
});