Les migrations posent le schéma et rien d'autre : une instance neuve n'a aucun compte et aucun utilisateur, donc personne ne peut se connecter. Le seed n'y remédie pas — il installe une démonstration et refuse de tourner en production, à raison. La première visite est désormais redirigée vers `/installation` : nom de l'entreprise, premier établissement avec son fuseau, et compte administrateur. L'écran crée le compte, le catalogue des capacités, les cinq rôles fournis, le propriétaire et son périmètre, puis ouvre la session par le chemin ordinaire — le rôle propriétaire exige aussitôt un second facteur, comme il se doit. Il ne se rouvre pas. Une table `Installation` d'une seule ligne, contrainte en base et protégée par un trigger append-only, marque l'instance. Elle est délibérément hors RLS, et c'est sa raison d'être : la politique d'`Account` ne laisse voir que le compte courant, si bien qu'une instance installée paraîtrait vierge à qui n'a pas de session — et la création d'un propriétaire se rouvrirait à tout venant. Le recensement des politiques porte l'exception, affirmée dans les deux sens. Deux défauts trouvés en éprouvant l'écran sur une base réellement vierge : `INSERT ... RETURNING` sur `Account` était refusé. L'insertion est permise, mais la relecture de la ligne écrite passe par la politique de lecture, qui exige un compte courant. L'identifiant est donc tiré côté application et annoncé avant la création — la règle de partout, appliquée à la transaction qui crée le compte. Et un défaut qui dépassait cet écran : React 19 vide les champs non contrôlés dès qu'une action se termine, refus compris. Les champs vidés portant `required`, le clic suivant était arrêté par la validation du navigateur avant d'émettre un `submit` — le formulaire paraissait mort. Le formulaire de connexion en souffrait aussi ; la suite l'avait manqué parce qu'aucun test ne soumettait deux fois de suite. `PersistentForm` photographie la saisie à l'envoi et la rétablit, mots de passe exclus. Au passage, `Field` rattache son indication par `aria-describedby` : placée dans le `<label>`, elle entrait dans le nom accessible du champ. Éprouvé sur une base vierge, image de production, rôle NOSUPERUSER NOBYPASSRLS : redirection, trois refus motivés, installation, second facteur exigé, écran refermé pour le propriétaire comme pour un visiteur. 450 tests unitaires et d'intégration, 77 tests de bout en bout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
39 lines
1.6 KiB
TypeScript
39 lines
1.6 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
||
|
||
/**
|
||
* Première installation — PLAN.md §5.
|
||
*
|
||
* La base de cette suite est semée, donc **installée**. Ce que ces tests
|
||
* peuvent prouver est justement ce qui compte le plus : l'écran ne se rouvre
|
||
* pas. Il crée un propriétaire sans demander de session ; laissé accessible
|
||
* après coup, il donnerait à n'importe quel visiteur un compte disposant de
|
||
* tous les droits sur l'instance.
|
||
*
|
||
* La création elle-même est éprouvée contre une base réelle dans
|
||
* tests/integration/installation.test.ts, où la transaction est annulée — le
|
||
* marqueur d'installation, lui, ne se supprime pas.
|
||
*/
|
||
|
||
test('l’écran d’installation est refermé sur une instance installée', async ({
|
||
page,
|
||
}) => {
|
||
await page.goto('/installation');
|
||
|
||
// Redirigé vers la connexion, et non pas seulement privé de son formulaire.
|
||
await expect(page).toHaveURL(/\/connexion/);
|
||
await expect(page.getByRole('heading', { name: 'Connexion' })).toBeVisible();
|
||
|
||
// Le formulaire ne doit pas non plus subsister dans le HTML : un champ caché
|
||
// reste soumettable.
|
||
expect(await page.content()).not.toContain('Première installation');
|
||
});
|
||
|
||
test('la connexion reste la porte d’entrée', async ({ page }) => {
|
||
// Le pendant du test précédent : sur une instance installée, la racine ne
|
||
// doit pas dériver vers l'installation. Les deux redirections se répondent,
|
||
// et une erreur de sens les ferait boucler l'une sur l'autre.
|
||
await page.goto('/');
|
||
await expect(page).toHaveURL(/\/connexion/);
|
||
await expect(page.getByRole('heading', { name: 'Connexion' })).toBeVisible();
|
||
});
|