La matrice n° 15 l'impose ; les colonnes existaient en base depuis WP-01 mais rien ne les remplissait. TOTP (RFC 6238) plutôt qu'un code envoyé par message : le second facteur ne doit pas dépendre du canal qui sert déjà à réinitialiser le mot de passe, faute de quoi un accès à la boîte donnerait les deux d'un coup. L'algorithme est écrit ici plutôt qu'emprunté — trente lignes, et une dépendance de moins sur le chemin d'authentification. Il est éprouvé contre les vecteurs publiés de la RFC, ce qui distingue « le code change toutes les trente secondes » de « le code est celui qu'attend l'application du téléphone ». L'obligation est adossée aux capacités, pas à des rôles nommés qu'un client renomme librement : distribuer les droits, ou lire les rémunérations. Elle est tenue au point de passage de toutes les routes applicatives, où l'écran d'enrôlement remplace le contenu. Il remplace plutôt qu'il ne redirige : une redirection depuis un layout se joue aussi pendant la navigation qui suit la connexion, et Next y répond par une page vide. Le secret n'est enregistré qu'après qu'un code en a été tiré — l'enregistrer d'avance laisserait des comptes porteurs d'un facteur que leur détenteur ne sait pas produire, c'est-à-dire des comptes fermés. Le rejeu d'un code est refusé dans sa propre fenêtre : sans cela le facteur protège d'un mot de passe volé, pas d'un code lu par-dessus l'épaule. Dix codes de secours accompagnent chaque activation, conservés hachés. Et parce que PlanFlow est auto-hébergé et qu'il n'y a pas d'éditeur à appeler, un retrait depuis le serveur existe — l'accès « break glass » que demande la même ligne de la matrice. Sans lui, le second facteur deviendrait le risque principal plutôt que la protection. Un défaut trouvé par les tests, et il aurait été grave : la réactualisation de la route après activation remplaçait l'écran par celui d'un compte déjà enrôlé, emportant les codes de secours avant que leur destinataire ait pu les noter. La suite de tests suit le produit : la mise en place enrôle réellement le compte de direction et calcule les codes comme le ferait un téléphone. Les écrans qui n'éprouvent pas l'authentification ont migré vers la session commune — deux connexions parallèles sur un même compte se heurtent au refus de rejeu, ce qui est le comportement voulu et non un défaut à contourner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
/**
|
|
* Registre de paramétrage et création d'un salarié sans accès applicatif.
|
|
*
|
|
* Ces parcours n'éprouvent pas l'authentification : ils réutilisent la session
|
|
* commune plutôt que d'ouvrir la leur. La direction porte un second facteur, et
|
|
* un code TOTP ne sert qu'une fois — deux connexions parallèles sur ce compte
|
|
* échoueraient, à raison.
|
|
*/
|
|
|
|
test('la direction lit et alimente le registre de paramétrage', async ({
|
|
page,
|
|
}) => {
|
|
await page.goto('/reglages/registre');
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Registre de paramétrage juridique' }),
|
|
).toBeVisible();
|
|
|
|
const parameter = `Durée quotidienne maximale ${Date.now()}`;
|
|
// Ciblage par attribut `name` : les libellés portent un texte d'aide, et
|
|
// celui de « Source » contient lui-même le mot « valeur », ce qui rend la
|
|
// correspondance par libellé ambiguë.
|
|
const form = page.locator('form').filter({ hasText: 'Consigner' });
|
|
await form.locator('input[name="key"]').fill(parameter);
|
|
await form.locator('input[name="value"]').fill('10 h');
|
|
await form
|
|
.locator('input[name="source"]')
|
|
.fill('IDCC 1517 — texte consolidé Legifrance');
|
|
await form.locator('input[name="population"]').fill('Tous les salariés');
|
|
await page.getByRole('button', { name: 'Consigner' }).click();
|
|
|
|
const row = page.getByRole('row', { name: new RegExp(parameter) });
|
|
await expect(row).toBeVisible();
|
|
|
|
// Consigné n'est pas approuvé : la matrice exige un approbateur nommé.
|
|
await row.getByRole('button', { name: 'Approuver' }).click();
|
|
await expect(row.getByText(/Approuvé le/)).toBeVisible();
|
|
});
|
|
|
|
test('un salarié sans compte applicatif est créable', async ({ page }) => {
|
|
await page.goto('/equipe');
|
|
|
|
await expect(page.getByRole('heading', { name: 'Équipe' })).toBeVisible();
|
|
// L'effectif vient de la base, pas du module de démonstration.
|
|
await expect(page.getByText('E0001')).toBeVisible();
|
|
|
|
// Nom unique par exécution : la base de test n'est pas remise à zéro entre
|
|
// deux passages, et un nom fixe finirait par désigner plusieurs salariés.
|
|
const matricule = `E9${Date.now() % 100000}`;
|
|
const nom = `Sanscompte${matricule}`;
|
|
const form = page.locator('form').filter({ hasText: 'Ajouter' });
|
|
await form.locator('input[name="firstName"]').fill('Sans');
|
|
await form.locator('input[name="lastName"]').fill(nom);
|
|
await form.locator('input[name="employeeNumber"]').fill(matricule);
|
|
await page.getByRole('button', { name: 'Ajouter' }).click();
|
|
await expect(page.getByText('Salarié ajouté.')).toBeVisible();
|
|
|
|
// Rechargement explicite : ce qui est vérifié ici est la persistance et la
|
|
// présence dans l'annuaire, pas le moment exact où la revalidation atteint
|
|
// le rendu courant.
|
|
await page.reload();
|
|
|
|
// Un salarié sans adresse doit exister : la plupart des équipes de vente ne
|
|
// se connectent jamais à l'outil.
|
|
await expect(
|
|
page.getByRole('cell', { name: matricule, exact: true }),
|
|
).toBeVisible();
|
|
// Le nom vit sur le dossier, pas sur le compte : un salarié sans accès
|
|
// applicatif doit tout de même figurer nommément au registre du personnel.
|
|
await expect(
|
|
page.getByRole('link', { name: new RegExp(`Sans ${nom}`) }),
|
|
).toBeVisible();
|
|
});
|