Files
planflow/tests/e2e/auth.spec.ts
T
Claude 2c0e9e8dd4 Wire authentication into the application
Adds the sign-in screen, sign-out, and a server-side guard on every
application route. The guard lives in the layout rather than the proxy
because the proxy cannot query the database to check whether a session
was revoked — and revocation is the reason sessions are stored there.

Sign-in returns one message for an unknown account and for a wrong
password, and verifies a dummy hash when the account does not exist, so
neither the wording nor the timing enumerates staff addresses. An
end-to-end test compares the two messages rather than trusting the
code to keep them aligned.

The shell now shows the signed-in person and their role from the
database instead of hardcoded initials.

Playwright signs in once in a setup project and shares the cookie;
argon2 is deliberately slow, and logging in per test would also drive
the shared failed-attempt counter toward a lockout. The seed resets
that counter so repeated local runs cannot lock the demo account.

Two test locators had to be scoped to the form: Next's route announcer
carries role="alert" and an empty string, which silently satisfied the
assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-07 22:45:24 +00:00

59 lines
2.4 KiB
TypeScript

import { expect, test } from '@playwright/test';
const EMAIL = 'direction@example.test';
const PASSWORD = 'planflow-demo-2026';
test('une route applicative redirige vers la connexion', async ({ page }) => {
await page.goto('/planning/semaine');
await expect(page).toHaveURL(/\/connexion$/);
await expect(page.getByRole('heading', { name: 'Connexion' })).toBeVisible();
});
test('un mot de passe faux ne dit pas si le compte existe', async ({ page }) => {
async function attempt(email: string): Promise<string> {
// Une page neuve par tentative : le message précédent resterait sinon à
// l'écran et le test comparerait deux fois le même.
await page.goto('/connexion');
await page.getByLabel('Adresse électronique').fill(email);
await page.getByLabel('Mot de passe').fill('mauvais-mot-de-passe');
await page.getByRole('button', { name: 'Se connecter' }).click();
// Restreint au formulaire : Next pose un annonceur de route qui porte lui
// aussi role="alert" et qui est vide.
const alert = page.locator('form').getByRole('alert');
await expect(alert).toBeVisible();
return (await alert.textContent()) ?? '';
}
const knownAccount = await attempt(EMAIL);
const unknownAccount = await attempt('inconnu@example.test');
// Un message différent laisserait énumérer les adresses du personnel.
expect(unknownAccount).toBe(knownAccount);
expect(knownAccount).toContain('Identifiants incorrects');
});
test('connexion, navigation, puis déconnexion', async ({ page }) => {
await page.goto('/connexion');
await page.getByLabel('Adresse électronique').fill(EMAIL);
await page.getByLabel('Mot de passe').fill(PASSWORD);
await page.getByRole('button', { name: 'Se connecter' }).click();
await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible();
// L'identité affichée vient de la base, pas d'un libellé en dur.
await expect(page.getByTitle(/Camille Ferrand/)).toBeVisible();
await page.getByRole('link', { name: 'Plannings' }).click();
await expect(
page.getByRole('heading', { name: /Planning · semaine 33/ }),
).toBeVisible();
await page.getByRole('button', { name: 'Déconnexion' }).click();
await expect(page).toHaveURL(/\/connexion$/);
// La session est révoquée en base : revenir en arrière ne doit pas rouvrir
// l'application.
await page.goto('/equipe');
await expect(page).toHaveURL(/\/connexion$/);
});