mediaDrop/tests/auth-flow.test.ts
Nicolas 60d7ba1c39
All checks were successful
Publier l'image Docker / test (push) Successful in 17s
Publier l'image Docker / publish (push) Successful in 9s
Corrige la déconnexion : accepter le POST de formulaire (urlencoded)
Le <form> de déconnexion envoie `application/x-www-form-urlencoded` ;
Fastify n'ayant aucun parseur pour ce type répondait 415
(FST_ERR_CTP_INVALID_MEDIA_TYPE) avant même d'atteindre le handler.

Ajout d'un parseur `application/x-www-form-urlencoded` (URLSearchParams
-> objet, sans dépendance). Le test de déconnexion envoie désormais
l'en-tête du navigateur pour couvrir le cas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:48:15 +02:00

102 lines
3.1 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 (soumission de <form> HTML)', async () => {
const cookie = await login(app, idp);
// Le navigateur envoie un POST application/x-www-form-urlencoded : le serveur
// doit l'accepter (et non répondre 415).
const out = await app.inject({
method: 'POST',
url: '/auth/logout',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
payload: '',
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');
});
});