`GET /` décide côté serveur, à chaque requête, laquelle des deux pages servir : `public/login.html` (aucune session valide — juste le bouton « Se connecter », aucune zone de dépôt dans le HTML) ou `public/upload.html` (session valide — nom d'utilisateur injecté et échappé, bouton « Se déconnecter »). Nouveau `src/routes/pages.ts`. Simplifications côté page de dépôt, permises par cette garantie serveur : plus de fetch `/auth/me` ni de bascule `authenticated` en JS, plus de retrait/réinsertion de la zone de dépôt dans le DOM — elle est toujours présente puisque la page n'est servie qu'aux sessions valides. Une session qui expire en cours d'usage (401 sur /targets ou /upload*) renvoie vers `/?login=expired`, qui affiche alors la vraie page de connexion avec le message adapté. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||
|
||
import { buildApp } from '../src/app.js';
|
||
import type { OidcConfig } from '../src/config.js';
|
||
import {
|
||
login,
|
||
makeConfig,
|
||
makeTempDir,
|
||
oneTarget,
|
||
removeDir,
|
||
SESSION_COOKIE,
|
||
startStubIdp,
|
||
type StubIdp,
|
||
} from './helpers.js';
|
||
|
||
let idp: StubIdp;
|
||
let app: FastifyInstance;
|
||
let uploadDir: string;
|
||
|
||
function oidcOverride(ref: StubIdp): OidcConfig {
|
||
return {
|
||
issuer: ref.issuer,
|
||
clientId: ref.clientId,
|
||
clientSecret: ref.clientSecret,
|
||
scopes: ['openid', 'profile', 'email'],
|
||
};
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
idp = await startStubIdp();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await idp.close();
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
uploadDir = await makeTempDir();
|
||
app = await buildApp(makeConfig({ targets: oneTarget(uploadDir), oidc: oidcOverride(idp) }), {
|
||
logger: false,
|
||
});
|
||
await app.ready();
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await app.close();
|
||
await removeDir(uploadDir);
|
||
});
|
||
|
||
describe('GET /', () => {
|
||
it('sans session : sert la page de connexion, pas la page de dépôt', async () => {
|
||
const res = await app.inject({ method: 'GET', url: '/' });
|
||
expect(res.statusCode).toBe(200);
|
||
expect(res.headers['content-type']).toContain('text/html');
|
||
expect(res.body).toContain('href="/auth/login"');
|
||
expect(res.body).not.toContain('id="dropzone"');
|
||
expect(res.body).not.toContain('Se déconnecter');
|
||
});
|
||
|
||
it('avec session valide : sert la page de dépôt avec le nom injecté', async () => {
|
||
const cookie = await login(app, idp, { name: 'Alice' });
|
||
const res = await app.inject({
|
||
method: 'GET',
|
||
url: '/',
|
||
cookies: { [SESSION_COOKIE]: cookie },
|
||
});
|
||
expect(res.statusCode).toBe(200);
|
||
expect(res.body).toContain('Connecté : Alice');
|
||
expect(res.body).toContain('Se déconnecter');
|
||
expect(res.body).toContain('id="dropzone"');
|
||
expect(res.body).not.toContain('{{USER_NAME}}');
|
||
expect(res.body).not.toContain('href="/auth/login"');
|
||
});
|
||
|
||
it('échappe le nom d’utilisateur avant de l’insérer dans le HTML', async () => {
|
||
const cookie = await login(app, idp, { name: '<script>alert(1)</script>' });
|
||
const res = await app.inject({
|
||
method: 'GET',
|
||
url: '/',
|
||
cookies: { [SESSION_COOKIE]: cookie },
|
||
});
|
||
expect(res.body).not.toContain('<script>alert(1)</script>');
|
||
expect(res.body).toContain('<script>alert(1)</script>');
|
||
});
|
||
|
||
it('session invalide (cookie illisible) : retombe sur la page de connexion', async () => {
|
||
const res = await app.inject({
|
||
method: 'GET',
|
||
url: '/',
|
||
cookies: { [SESSION_COOKIE]: 'cookie-bidon' },
|
||
});
|
||
expect(res.statusCode).toBe(200);
|
||
expect(res.body).toContain('href="/auth/login"');
|
||
expect(res.body).not.toContain('id="dropzone"');
|
||
});
|
||
});
|