Run the direct sign-up e2e scenarios on disposable accounts deleted after each scenario

This commit is contained in:
Sylvain Duchesne
2026-11-06 23:24:57 +01:00
parent 9169a5817c
commit ebcf5f4266
3 changed files with 53 additions and 15 deletions
@@ -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"
@@ -11,6 +11,7 @@ import {
memberUrnOf,
participationSecretOf,
participationsCollectionOf,
podLogin,
postOutboxActivity,
readAttendees,
readResource,
@@ -90,12 +91,45 @@ async function registered(world: FestipodWorld): Promise<boolean> {
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<Response> {
return fetch(`${apodsStackConfig().podProviderUrl}${path}`, { ...init, signal: AbortSignal.timeout(30000) });
}
async function deleteAccount(username: string, token: string | undefined): Promise<void> {
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'),
+2
View File
@@ -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;