Files
ng-eventually/packages/polyfill/test/ng-proxy.test.ts
T
Sylvain Duchesne 12eba6eea6 fix: quatre écarts entre la surface publiée et ce que NextGraph déclare
Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.

L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).

sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.

ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.

Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.

docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.

Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
2026-08-14 10:00:40 +02:00

92 lines
3.5 KiB
TypeScript

import { getCaps } from "../src/shared-wallet/bootstrap";
import { test, expect, mock, afterEach } from "bun:test";
import { makeNg } from "../src/surface/ng-proxy";
import { configure } from "../src/index";
import { setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig } from "../src/shared-wallet/bootstrap";
// This suite injects a fake `ng` via configure() and declares WRITE caps —
// which stay an authorization list on purpose: only READING is key possession
// (delivered by cap-surface). The write axis is decorative until
// cap-enforcement (every internal writer bypasses this proxy). Reset after each
// test so the docs.test.ts "not configured" guard still holds and no cap leaks
// into another suite.
afterEach(() => {
resetConfig();
resetCaps();
setCurrentUser(null);
});
function fakeNg() {
return { sparql_update: mock(async (..._a: unknown[]) => undefined) };
}
function inject() {
const ng = fakeNg();
configure({ ng: ng as any, useShape: (() => {}) as any });
return ng;
}
const DOC = "did:ng:o:doc";
const UPDATE = `INSERT DATA { GRAPH <${DOC}> { <s> <p> <o> } }`;
test("write guard: passthrough when NO write policy is declared (no regression)", async () => {
const ng = inject();
setCurrentUser("bob"); // not a writer, but there's no policy at all
const proxy = makeNg();
await proxy.sparql_update("sid", UPDATE, DOC);
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
});
test("write guard: passthrough for an UNGOVERNED doc even when a policy exists elsewhere", async () => {
const ng = inject();
getCaps().grantWrite("did:ng:o:other", "alice"); // policy on another doc
setCurrentUser("bob");
const proxy = makeNg();
await proxy.sparql_update("sid", UPDATE, DOC); // DOC itself is ungoverned
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
});
test("write guard: REJECTS when the doc is governed and the user lacks the write cap", async () => {
const ng = inject();
getCaps().grantWrite(DOC, "alice"); // alice holds the write cap
setCurrentUser("bob"); // bob does not
const proxy = makeNg();
await expect(proxy.sparql_update("sid", UPDATE, DOC)).rejects.toThrow(
/write denied/,
);
expect(ng.sparql_update).toHaveBeenCalledTimes(0); // never reached the real ng
});
test("write guard: REJECTS an anonymous (null) user on a governed doc", async () => {
const ng = inject();
getCaps().grantWrite(DOC, "alice");
setCurrentUser(null);
const proxy = makeNg();
await expect(proxy.sparql_update("sid", UPDATE, DOC)).rejects.toThrow(
/write denied/,
);
expect(ng.sparql_update).toHaveBeenCalledTimes(0);
});
test("write guard: ALLOWS the write-cap holder", async () => {
const ng = inject();
getCaps().grantWrite(DOC, "alice");
setCurrentUser("alice"); // owner always holds the write cap
const proxy = makeNg();
await proxy.sparql_update("sid", UPDATE, DOC);
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
});
test("write guard: passthrough when anchor is omitted (cannot scope the guard)", async () => {
const ng = inject();
getCaps().grantWrite(DOC, "alice");
setCurrentUser("bob");
const proxy = makeNg();
// Anchor explicitly `undefined` — upstream declares all three parameters (`index.d.ts:297`),
// and the guard reads `args[2]`, which is `undefined` whether the argument is omitted or
// passed as such. Same branch, same passthrough.
await proxy.sparql_update("sid", "INSERT DATA {}", undefined); // no anchor → passthrough
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
});