From ebcf5f4266f707239edbe9bafc7966184e87a004 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Fri, 6 Nov 2026 23:24:57 +0100 Subject: [PATCH] Run the direct sign-up e2e scenarios on disposable accounts deleted after each scenario --- .../event/features/e2e-activitypods.feature | 19 ++++---- .../steps/e2e/inscription-evenement.steps.ts | 47 ++++++++++++++++--- src/shared/support/world.ts | 2 + 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/modules/event/features/e2e-activitypods.feature b/src/modules/event/features/e2e-activitypods.feature index 6a165a3..8f9a8c1 100644 --- a/src/modules/event/features/e2e-activitypods.feature +++ b/src/modules/event/features/e2e-activitypods.feature @@ -66,12 +66,13 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale # point de rencontre : l'URN de membre (ancrée sur l'événement) entre dans la # collection `apods:attendees` de l'événement, et l'URI de l'événement dans la # collection des participations de la session. L'inscription à l'événement est - # dérivée : directe, ou membre d'un de ses points de rencontre. Chaque scénario quitte - # ce qu'il a rejoint, même en cas d'échec. + # dérivée : directe, ou membre d'un de ses points de rencontre. Chaque scénario tourne sur un compte + # jetable, créé puis supprimé (Pod compris) même en cas d'échec : rien ne reste dans + # le pod d'un compte persistant. @inscription-evenement Scénario: Rejoindre puis quitter un événement met à jour sa collection apods:attendees - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un événement "Concert direct" avec sa collection apods:attendees Quand la session s'inscrit à l'événement @@ -84,7 +85,7 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale @inscription-evenement Scénario: L'inscription directe et l'inscription à un point de rencontre partagent la même URN de membre - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un point de rencontre "Rdv entrée" rattaché à un événement, avec sa collection apods:attendees Et l'événement a aussi sa collection apods:attendees @@ -96,7 +97,7 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale @inscription-evenement Scénario: Quitter le dernier point de rencontre sans inscription directe désinscrit de l'événement - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un point de rencontre "Rdv entrée" rattaché à un événement, avec sa collection apods:attendees Quand la session rejoint le point de rencontre @@ -107,7 +108,7 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale @inscription-evenement Scénario: Quitter le dernier point de rencontre garde l'inscription directe - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un point de rencontre "Rdv entrée" rattaché à un événement, avec sa collection apods:attendees Et l'événement a aussi sa collection apods:attendees @@ -120,7 +121,7 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale @inscription-evenement Scénario: Se désinscrire de l'événement retire l'inscription directe et celles des points de rencontre - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un point de rencontre "Rdv entrée" rattaché à un événement, avec sa collection apods:attendees Et l'événement a aussi sa collection apods:attendees @@ -134,11 +135,11 @@ Fonctionnalité: Smoke e2e de la pile ActivityPods locale @browser @inscription-evenement Scénario: Parcours : s'inscrire à un événement, rejoindre un point de rencontre, se désinscrire de l'événement avec confirmation - Étant donné une session Pod locale pour "alice" avec le mot de passe "Test1test" + Étant donné une session Pod jetable avec le mot de passe "Test1test" Et l'application Festipod est installée sur le Pod de la session Et un point de rencontre "Rdv parcours" rattaché à un événement, avec sa collection apods:attendees Et l'événement a aussi sa collection apods:attendees - Et "alice" est connectée dans l'application avec le mot de passe "Test1test" + Et la session est connectée dans l'application avec le mot de passe "Test1test" Quand dans l'application, j'ouvre l'événement Et dans l'application, j'appuie sur "S'inscrire à l'événement" Alors l'application affiche "✓ Inscrit à l'événement" diff --git a/src/modules/event/steps/e2e/inscription-evenement.steps.ts b/src/modules/event/steps/e2e/inscription-evenement.steps.ts index 79637ec..3632dbd 100644 --- a/src/modules/event/steps/e2e/inscription-evenement.steps.ts +++ b/src/modules/event/steps/e2e/inscription-evenement.steps.ts @@ -11,6 +11,7 @@ import { memberUrnOf, participationSecretOf, participationsCollectionOf, + podLogin, postOutboxActivity, readAttendees, readResource, @@ -90,12 +91,45 @@ async function registered(world: FestipodWorld): Promise { return targets(world).some(uri => items.includes(uri)); } -After({ tags: '@inscription-evenement', timeout: 60000 }, async function (this: FestipodWorld) { - if (!this.apods.session) return; - const to = appActorUri(apodsStackConfig()); - for (const uri of targets(this)) { - await postOutboxActivity(this.apods.session, 'Leave', uri, to).catch(() => undefined); +/** + * The scenarios run on a throwaway account the run creates and deletes: nothing they + * write (events, meeting points, activities, secrets) may stay in a persistent + * account's pod (rule_tests-never-break-the-dev-environment). Deleting the account + * is the cleanup — the platform drops its dataset — and it runs whatever failed. + */ +const POD_PASSWORD = 'Test1test'; + +async function podApi(path: string, init: RequestInit): Promise { + return fetch(`${apodsStackConfig().podProviderUrl}${path}`, { ...init, signal: AbortSignal.timeout(30000) }); +} + +async function deleteAccount(username: string, token: string | undefined): Promise { + let bearer = token; + if (!bearer) { + // The sign-up may have succeeded before the login failed: log in again to get a token. + const login = await podApi('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password: POD_PASSWORD }), + }).catch(() => undefined); + if (!login?.ok) return; + bearer = ((await login.json().catch(() => ({}))) as { token?: string }).token; } + if (!bearer) return; + await podApi(`/.account/${username}`, { method: 'DELETE', headers: { Authorization: `Bearer ${bearer}` } }).catch(() => undefined); +} + +Given('une session Pod jetable avec le mot de passe {string}', async function (this: FestipodWorld, password: string) { + const username = `fpjet${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; + expect(password, 'the cleanup logs in with the shared test password').to.equal(POD_PASSWORD); + this.apods.username = username; // recorded before the sign-up so the After hook deletes it even if the login fails + this.apods.session = await podLogin(apodsStackConfig(), username, password); +}); + +After({ tags: '@inscription-evenement', timeout: 120000 }, async function (this: FestipodWorld) { + const username = this.apods.username; + if (!username) return; + await deleteAccount(username, this.apods.session?.token); }); Given('un événement {string} avec sa collection apods:attendees', async function (this: FestipodWorld, title: string) { @@ -183,7 +217,8 @@ Then('une relecture ultérieure ne réinscrit pas la session', async function (t const stackUrl = (value: string | undefined, fallback: string) => (value || fallback).replace(/\/+$/, ''); -Given('{string} est connectée dans l\'application avec le mot de passe {string}', async function (this: FestipodWorld, username: string, password: string) { +Given('la session est connectée dans l\'application avec le mot de passe {string}', async function (this: FestipodWorld, password: string) { + const username = required(this.apods.username, 'the throwaway account name'); const ctx: DriveContext = { app: required(this.apods.appUrl, 'app URL'), podFront: stackUrl(process.env.POD_FRONTEND_URL, 'http://localhost:5000'), diff --git a/src/shared/support/world.ts b/src/shared/support/world.ts index 1cf8d63..3d2f43e 100644 --- a/src/shared/support/world.ts +++ b/src/shared/support/world.ts @@ -16,6 +16,8 @@ export interface ApodsScenarioState { session?: ApodsSession; /** The app server URL the `@browser` scenario loads (set by the hook). */ appUrl?: string; + /** Name of the throwaway account a scenario created (deleted by its After hook). */ + username?: string; anchorUri?: string; meetingPointUri?: string; collectionUri?: string;