0455a408b6
L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :
- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
`getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
la session EST la connexion — aucune application n'attend un second appel.
Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.
Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.
180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
/**
|
|
* The access gate's identity resolution.
|
|
*
|
|
* This is the piece whose failure is SILENT: get the order wrong and the broker iframe
|
|
* reads an empty identity, provisions a second virtual user, and the returning user
|
|
* lands in an empty space with no error anywhere. So the order is pinned, not trusted.
|
|
*/
|
|
import { getCurrentUser } from "../src/shared-wallet/bootstrap";
|
|
import { test, expect, afterEach } from "bun:test";
|
|
import { configure } from "../src/index";
|
|
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
|
|
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import { ensureIdentity } from "../src/shared-wallet/access-gate";
|
|
|
|
const KEY = "ng-eventually:identity";
|
|
|
|
/** A localStorage double — the real one is absent in `bun test`. */
|
|
function fakeStorage(initial: Record<string, string> = {}) {
|
|
const map = new Map(Object.entries(initial));
|
|
return {
|
|
getItem: (k: string) => map.get(k) ?? null,
|
|
setItem: (k: string, v: string) => void map.set(k, v),
|
|
removeItem: (k: string) => void map.delete(k),
|
|
get size() { return map.size; },
|
|
};
|
|
}
|
|
|
|
/** Put the page in a given URL + storage state, as the browser would. */
|
|
function inPage(search: string, storage: ReturnType<typeof fakeStorage>) {
|
|
(globalThis as any).location = { search, href: "https://app.example" + search };
|
|
(globalThis as any).localStorage = storage;
|
|
(globalThis as any).history = { replaceState: () => {} };
|
|
}
|
|
|
|
afterEach(() => {
|
|
setCurrentUser(null);
|
|
resetConfig();
|
|
resetStoreRegistry();
|
|
delete (globalThis as any).location;
|
|
delete (globalThis as any).localStorage;
|
|
delete (globalThis as any).history;
|
|
});
|
|
|
|
function configured() {
|
|
configureStoreRegistry({
|
|
getSession: async () => ({ sessionId: "s", privateStoreId: "did:ng:o:p" }),
|
|
normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(),
|
|
});
|
|
configure({
|
|
ng: {} as never,
|
|
useShape: (() => {}) as never,
|
|
sharedWallet: { fileUrl: "/w.ngw", password: "pw" },
|
|
});
|
|
}
|
|
|
|
test("an identity already set is left alone — the gate never re-asks", async () => {
|
|
configured();
|
|
inPage("", fakeStorage());
|
|
setCurrentUser("alice");
|
|
await ensureIdentity();
|
|
expect(getCurrentUser()).toBe("alice");
|
|
});
|
|
|
|
test("the URL parameter WINS over storage — it is the only thing that crosses the frontier", async () => {
|
|
// The top-level page and the broker iframe have separate localStorage partitions, so a
|
|
// value written on one side is not the value the other reads. The URL survives the
|
|
// round-trip; storage does not. If storage won here, a user entering a second
|
|
// identifier would keep being sent back to the first one's space.
|
|
configured();
|
|
inPage("?ng-id=fromurl", fakeStorage({ [KEY]: "fromstorage" }));
|
|
await ensureIdentity();
|
|
expect(getCurrentUser()).toBe("fromurl");
|
|
});
|
|
|
|
test("the URL parameter is copied into THIS partition, so a plain reload still knows", async () => {
|
|
configured();
|
|
const storage = fakeStorage();
|
|
inPage("?ng-id=carol", storage);
|
|
await ensureIdentity();
|
|
expect(storage.getItem(KEY)).toBe("carol");
|
|
});
|
|
|
|
test("with no parameter, storage answers — a reload does not re-ask", async () => {
|
|
configured();
|
|
inPage("", fakeStorage({ [KEY]: "dana" }));
|
|
await ensureIdentity();
|
|
expect(getCurrentUser()).toBe("dana");
|
|
});
|
|
|
|
test("nothing known and no DOM to ask on → it refuses loudly", async () => {
|
|
// Continuing silently would provision an anonymous virtual space, which is the failure
|
|
// this module exists to prevent. The error names what the caller must do.
|
|
configured();
|
|
inPage("", fakeStorage());
|
|
await expect(ensureIdentity()).rejects.toThrow(/no DOM to ask on/i);
|
|
});
|
|
|
|
test("no shared wallet configured → it refuses, rather than inventing a space", async () => {
|
|
configure({ ng: {} as never, useShape: (() => {}) as never });
|
|
inPage("", fakeStorage());
|
|
await expect(ensureIdentity()).rejects.toThrow(/no shared wallet configured/i);
|
|
});
|
|
|
|
test("the URL value is NORMALIZED on the way in — `@Erin` and `erin` are one space", async () => {
|
|
// Ported from the consumer's `identifiant-resolution` feature, and it caught a real
|
|
// defect here: the gate normalized what a user TYPED but not what the URL carried, so
|
|
// a link with `?ng-id=@Erin` keyed onto a different virtual user than the same person
|
|
// typing `erin`. One normalizer — the injected one — for all three entry paths.
|
|
configured();
|
|
inPage("?ng-id=@Erin", fakeStorage());
|
|
await ensureIdentity();
|
|
expect(getCurrentUser()).toBe("erin");
|
|
});
|
|
|
|
test("a stored value is normalized too — an old entry cannot key onto a second space", async () => {
|
|
configured();
|
|
inPage("", fakeStorage({ [KEY]: "@Frank" }));
|
|
await ensureIdentity();
|
|
expect(getCurrentUser()).toBe("frank");
|
|
});
|