diff --git a/src/modules/event/features/reconnexion-meme-identite.feature b/src/modules/event/features/reconnexion-meme-identite.feature new file mode 100644 index 0000000..511e4d1 --- /dev/null +++ b/src/modules/event/features/reconnexion-meme-identite.feature @@ -0,0 +1,32 @@ +# language: fr +@EVENT @priority-1 @data @wip +Fonctionnalité: Reconnexion d'une même identité sur le wallet persistant + En tant qu'utilisateur qui, sur le MÊME wallet physique, ouvre une PAGE FRAÎCHE + (nouveau login broker, session fraîche) sous le MÊME identifiant qu'avant, + Je dois relire MES PROPRES données (mon événement, ma participation) + Afin que rien ne disparaisse à la reconnexion. + + # Régression de RECONNEXION (distincte de l'isolation deux-identités). @wip : le + # défaut est RÉEL mais non encore corrigé — ce scénario le documente et échoue + # tant que le fix n'est pas complet. + # + # SYMPTÔME observé (broker réel, mesuré) : une identité A crée E + s'y inscrit + # sur la page principale ; une PAGE FRAÎCHE pour la MÊME identité A (même wallet, + # nouveau login → session verifier fraîche) relit VIDE (home=[], + # isParticipating=false, count=0). La page fraîche DOIT retrouver E, sa + # participation, et un count autoritatif de 1. + # + # MÉCANISME (sous investigation, à confirmer sous broker) : la lecture ancrée à + # froid tape des repos pas encore ouverts. Le fix lib `open-repo` fait remonter + # le PROTECTED (participation) au cold-start, mais PAS l'accueil PUBLIC : + # `readScopeIndex` de l'index de scope public rend 0 (observé même côté écrivain + # même-session) alors que le code d'index de la lib est scope-symétrique — cause + # exacte encore à mesurer (broker requis). + + @data + Scénario: Une page fraîche pour la même identité relit ses propres données + Étant donné que l'identité A crée l'événement "Événement de reconnexion de A" et s'y inscrit + Quand une page fraîche pour la MÊME identité A recharge sur le même wallet + Alors l'événement "Événement de reconnexion de A" est sur l'accueil de la page fraîche A + Et la page fraîche A est participante de l'événement "Événement de reconnexion de A" + Et le compte autoritatif de participation de A à l'événement "Événement de reconnexion de A" est 1 diff --git a/src/modules/event/steps/data/reconnexion.steps.ts b/src/modules/event/steps/data/reconnexion.steps.ts new file mode 100644 index 0000000..dbeb883 --- /dev/null +++ b/src/modules/event/steps/data/reconnexion.steps.ts @@ -0,0 +1,84 @@ +import { When, Then } from '@cucumber/cucumber'; +import { expect } from 'chai'; +import type { FestipodWorld } from '../../../../shared/support/world'; +import { pool } from '../../../../shared/support/browserPool'; + +// RECONNECTION of the SAME identity on the SAME persistent wallet (@data, real +// broker). Distinct from the two-identity isolation scenario: here the fresh page +// re-enters under the SAME identifier A (same virtual wallet), on a NEW broker +// login (fresh verifier session). The defect under test: the fresh page reads its +// OWN data EMPTY (home=[], isParticipating=false, authCount=0) because the anchored +// listing path (listMyEntityDocs → readScopeIndex, then readUnion/readDoc) queries +// repos not yet in `self.repos` at cold-start and silently returns 0 rows. +// +// Identity A is the scenario's fresh virtual-wallet username (this.freshUser, set +// by the Before hook into localStorage on every origin). A creates E via the REAL +// app path (createEventReal) and joins it (appJoinEvent) on the MAIN page. Then a +// FRESH PAGE is brought up on the SAME persistent wallet context with the SAME +// identifier A in localStorage BEFORE any script — the closest analogue to the real +// app's re-enter-gate / reload path (a brand-new NgDataProvider mount + a fresh +// broker session that must re-open A's own repos). Montage identical to +// isolation.steps.ts, except the fresh page reuses this.freshUser (SAME A) rather +// than minting a new identifier B. + +// The Given "l'identité A crée l'événement {string} et s'y inscrit" is REUSED from +// isolation.steps.ts (same wording, same behavior — A creates E and joins on the +// main page). It stores this.isoEventId / this.isoAId, which the steps below read. + +When('une page fraîche pour la MÊME identité A recharge sur le même wallet', { timeout: 120000 }, async function (this: FestipodWorld) { + // SAME identity A as the main page: reuse the scenario's fresh virtual-wallet + // username (set by the Before hook). NOT a new identifier — this is a reconnect, + // not an identity switch. + const aIdentifier = (this as any).freshUser as string; + const ctx = this.page!.context(); + const freshPage = await ctx.newPage(); + await freshPage.addInitScript((u: string) => { + try { window.localStorage.setItem('festipod.account.username', u); } catch { /* opaque */ } + }, aIdentifier); + await freshPage.addInitScript(() => { + (globalThis as Record).__FESTIPOD_ACCESS_GATE_DISABLED__ = true; + }); + freshPage.on('console', (msg) => { if (msg.type() === 'error') console.error('[FreshApage console]', msg.text()); }); + // New broker login → fresh verifier session on the SAME persistent wallet. + const freshFrame = await pool.setupBrokerPage!(freshPage, pool.harnessUrl!); + await freshFrame.waitForFunction(() => (window as any).__testData?.ready === true, { timeout: 60000 }); + // Let A's listing effect + anchored union read run on the fresh session (this is + // exactly the cold-start read path the fix heals). + await freshFrame.evaluate(async () => { + const td = (window as any).__testData; + await td.ensureCurrentUser(); + await new Promise(r => setTimeout(r, 6000)); + }); + (this as any).recoFreshFrame = freshFrame; +}); + +Then('l\'événement {string} est sur l\'accueil de la page fraîche A', async function (this: FestipodWorld, title: string) { + const freshFrame = (this as any).recoFreshFrame; + const onHome = await freshFrame.evaluate((title: string) => { + const td = (window as any).__testData; + return td.homeEventTitles().includes(title); + }, title); + expect(onHome, `"${title}" MUST appear on the fresh A page's home (getUserEvents(A) after reconnect)`).to.be.true; +}); + +Then('la page fraîche A est participante de l\'événement {string}', async function (this: FestipodWorld, title: string) { + const freshFrame = (this as any).recoFreshFrame; + const eventId = (this as any).isoEventId; + const isPart = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => { + const td = (window as any).__testData; + const aId = await td.ensureCurrentUser(); + return td.isParticipating(eventId, aId); + }, { eventId }); + expect(isPart, `The fresh A page MUST read its own participation in "${title}" after reconnect`).to.be.true; +}); + +Then('le compte autoritatif de participation de A à l\'événement {string} est {int}', { timeout: 60000 }, async function (this: FestipodWorld, _title: string, expected: number) { + const freshFrame = (this as any).recoFreshFrame; + const eventId = (this as any).isoEventId; + const count = await freshFrame.evaluate(async ({ eventId }: { eventId: string }) => { + const td = (window as any).__testData; + const aId = await td.ensureCurrentUser(); + return td.authParticipationCount(eventId, aId); + }, { eventId }); + expect(count, `Authoritative broker count of A's participation must be ${expected} after reconnect`).to.equal(expected); +});