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
59 lines
2.4 KiB
TypeScript
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$/);
|
|
});
|