Ng eventually #1
@@ -0,0 +1,7 @@
|
||||
# Doc-debt — app-security
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/modules/auth/steps/data/connexion.steps.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -8,3 +8,5 @@
|
||||
- TOUCHED src/modules/workshop/features/read-model-probe.feature @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/workshop/steps/data/read-model-probe.steps.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/data/inscription.steps.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/support/hooks.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/steps/data/connexion.steps.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/shared/data/readEntities.ts @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-05 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/utils/ngBootstrap.ts @2026-07-06 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
|
||||
@@ -5,27 +5,18 @@ import type { FestipodWorld } from '../../../../shared/support/world';
|
||||
// --- Setup ---
|
||||
|
||||
Given('le portefeuille est vide', async function (this: FestipodWorld) {
|
||||
// Empty the wallet for real: with one-document-per-entity + a persistent broker,
|
||||
// deleting entities means clearing the per-entity documents' CONTENT (the SDK
|
||||
// clearWallet), not just flipping a store-root set. Then poll until the reactive
|
||||
// read reflects the empty state.
|
||||
await this.appFrame!.evaluate(async () => {
|
||||
const td = (window as any).__testData;
|
||||
await td.clearWallet();
|
||||
});
|
||||
await this.appFrame!.waitForFunction(
|
||||
() => {
|
||||
const td = (window as any).__testData;
|
||||
return td.events.size === 0 && td.users.size === 0;
|
||||
},
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
// Each @data scenario runs under a UNIQUE username (see hooks.ts
|
||||
// freshScenarioUsername), so the shim hands it a FRESH, EMPTY virtual wallet:
|
||||
// "le portefeuille est vide" is trivially true on entry. So this is a fast
|
||||
// INSTANT CHECK — assert the reactive read already shows nothing — NOT the old
|
||||
// `clearWallet` per-entity-doc fan-out (a full physical-wallet enumeration that
|
||||
// was itself slow). No mutation, no polling: a fresh wallet has no docs to scan.
|
||||
const counts = await this.appFrame!.evaluate(() => {
|
||||
const td = (window as any).__testData;
|
||||
return { events: td.events.size, users: td.users.size };
|
||||
});
|
||||
expect(counts.events, 'Events should be empty').to.equal(0);
|
||||
expect(counts.users, 'Users should be empty').to.equal(0);
|
||||
expect(counts.events, 'Fresh virtual wallet should have no events').to.equal(0);
|
||||
expect(counts.users, 'Fresh virtual wallet should have no users').to.equal(0);
|
||||
});
|
||||
|
||||
Given('le portefeuille contient déjà des événements', async function (this: FestipodWorld) {
|
||||
|
||||
+38
-19
@@ -9,6 +9,24 @@ import { pool } from './browserPool';
|
||||
|
||||
setDefaultTimeout(90000);
|
||||
|
||||
// PER-SCENARIO FRESH VIRTUAL WALLET (T03.k). The shim keys each emulated account
|
||||
// (its own private virtual wallet) by the NORMALIZED app-level username read from
|
||||
// localStorage['festipod.account.username'] on the harness origin. When every
|
||||
// @data scenario logs in as the SAME fixed user, that ONE virtual wallet
|
||||
// accumulates every doc any prior scenario/run ever wrote → per-doc anchored
|
||||
// reads fan out over hundreds of docs → 90s timeouts. Giving each scenario a
|
||||
// UNIQUE username hands it a FRESH, EMPTY virtual wallet, so reads stay O(what
|
||||
// THIS scenario provisions) and are fast + independent. A monotonic counter +
|
||||
// per-run nonce guarantees uniqueness within and across runs; it normalizes to
|
||||
// itself (lowercase, `@`-free) and is disjoint from the reserved `@index`
|
||||
// account (whose shim key uses a sentinel prefix `normalizeUsername` can't emit).
|
||||
const RUN_NONCE = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
let scenarioSeq = 0;
|
||||
function freshScenarioUsername(): string {
|
||||
scenarioSeq += 1;
|
||||
return `test-${RUN_NONCE}-${scenarioSeq}`;
|
||||
}
|
||||
|
||||
let browser: Browser;
|
||||
let browserContext: BrowserContext;
|
||||
// Non-persistent launcher for fresh, isolated contexts (multi-browser scenarios).
|
||||
@@ -561,6 +579,20 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) {
|
||||
// the run self-heals instead of cascading failures across the rest.
|
||||
this.page = await newWalletPageResilient();
|
||||
|
||||
// FRESH VIRTUAL WALLET per scenario (see freshScenarioUsername above). Set a
|
||||
// UNIQUE app-level username into localStorage['festipod.account.username'] on
|
||||
// EVERY origin (the init script runs in each frame before its scripts do —
|
||||
// including the harness iframe on 127.0.0.1). At mount the harness's
|
||||
// AccountStore.get() then reads THIS fresh username, so `if (!username)
|
||||
// login(DEFAULT_HARNESS_USER)` is skipped and the scenario runs on a fresh,
|
||||
// empty virtual wallet. Overwrites any value persisted in the Chromium profile
|
||||
// (init scripts run on each navigation), so no accumulated wallet leaks in.
|
||||
const freshUser = freshScenarioUsername();
|
||||
(this as any).freshUser = freshUser;
|
||||
await this.page.addInitScript((u: string) => {
|
||||
try { window.localStorage.setItem('festipod.account.username', u); } catch { /* opaque origin */ }
|
||||
}, freshUser);
|
||||
|
||||
// Capture console for debugging
|
||||
this.page.on('pageerror', (err) => console.error('[Browser error]', err.message));
|
||||
this.page.on('console', (msg) => {
|
||||
@@ -579,25 +611,12 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) {
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
|
||||
// PER-SCENARIO STATE ISOLATION (T03.j). The @data suite shares ONE
|
||||
// persistent broker-backed wallet, so the emulated account registry
|
||||
// ACCUMULATES every account any prior scenario/run created — growing the
|
||||
// read fan-out (`allAccounts()` → per-account `listEntityDocs`) until it
|
||||
// gets slow and flaky. Purge the registry anchor once here so each @data
|
||||
// scenario starts from a CLEAN registry and the fan-out stays bounded to
|
||||
// what this scenario re-provisions. A single SPARQL DELETE on ONE graph —
|
||||
// not a fan-out delete.
|
||||
// HARD-BOUNDED (≤10s): this reset shares the Before hook's 60s budget with
|
||||
// the (already slow, intermittent) broker login. It must NEVER contend for
|
||||
// that budget — a slow purge on a hugely-accumulated anchor graph, or a
|
||||
// broker stall, is swallowed and the scenario proceeds (its own steps still
|
||||
// gate on state). So race it against a 10s cap and never let it throw.
|
||||
await Promise.race([
|
||||
this.appFrame.evaluate(async () => {
|
||||
try { await (window as any).__testData?.resetDataState?.(); } catch { /* best-effort */ }
|
||||
}),
|
||||
new Promise((r) => setTimeout(r, 10000)),
|
||||
]).catch(() => { /* best-effort */ });
|
||||
// NO per-scenario registry/wallet reset needed anymore (was T03.j
|
||||
// resetDataState). Each @data scenario now runs under a UNIQUE username
|
||||
// (freshScenarioUsername, set into localStorage above), so the shim hands it
|
||||
// a FRESH, EMPTY virtual wallet whose account registry starts empty by
|
||||
// construction — nothing to purge. This also drops the ≤10s reset cost that
|
||||
// shared the Before hook's budget with the (slow) broker login.
|
||||
} else {
|
||||
// Mock mode: load harness directly
|
||||
await this.page!.setContent('<!DOCTYPE html><html><body><div id="root"></div></body></html>');
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
seedUsers,
|
||||
} from '../data/seedData';
|
||||
import { writeEntity, ENTITY_TYPE, str, int, flt, bool } from '../data/entityWrites';
|
||||
import { submitEventToIndex } from '../data/discovery';
|
||||
|
||||
/** Scope of a seed entity + how to create its own document (SDK create). */
|
||||
export type Scope = 'public' | 'protected' | 'private';
|
||||
@@ -105,6 +106,12 @@ export async function bootstrapWallet(
|
||||
coverImage: str(e.coverImage), hostName: str(e.hostName), hostInitials: str(e.hostInitials),
|
||||
});
|
||||
eventIdMap.set(e.id, id);
|
||||
// Make the seeded PUBLIC event discoverable, exactly like the product's
|
||||
// createEvent: submit its reference to the global discovery index. Awaited so
|
||||
// the index is populated before any read (a fresh virtual wallet has no "own"
|
||||
// seed docs — it sees the seeded events only through discovery).
|
||||
await submitEventToIndex({ doc: graph, id, title: e.title }, null)
|
||||
.catch(err => console.error('[Bootstrap] submit seed event to index failed:', err));
|
||||
}));
|
||||
console.log('[Bootstrap] Seeded', eventIdMap.size, 'events (participations created live)');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user