import { test, expect, mock, beforeEach } from "bun:test"; import { docCreate, sparqlUpdate, sparqlQuery } from "../src/surface/docs"; // The reach guard is process-wide: once ANY cap exists it applies to every reader. // This suite declares none, so it must not inherit another suite's enforcement. beforeEach(() => { resetCaps(); setCurrentUser(null); }); import * as ngProxy from "../src/surface/ng-proxy"; // NOTE ORDER: the "not configured → throw" case MUST run before any configure() // call, because configure() sets a module-level singleton with no public reset. test("throws a clear error when configure() was not called", async () => { await expect(docCreate("sid", "Graph", "data:graph", "store")).rejects.toThrow( /configure\(\) must be called before use/, ); await expect(sparqlUpdate("sid", "INSERT DATA {}")).rejects.toThrow( /configure\(\) must be called before use/, ); await expect(sparqlQuery("sid", "SELECT * {}")).rejects.toThrow( /configure\(\) must be called before use/, ); }); // From here on, a fake real `ng` is injected via configure(). import { configure } from "../src/index"; import { setCurrentUser } from "../src/shared-wallet/bootstrap"; import { resetCaps } from "../src/shared-wallet/bootstrap"; // What a real `ng.sparql_update` answers: the COMMITS the update produced // (`sdk/js/lib-wasm/src/lib.rs:481-483` serialises `AppResponseV0::Commits`). The fake // used to answer `undefined` — a result the broker never returns — which is precisely // what let the surface declare `Promise` unchallenged. const COMMITS = [{ id: "did:ng:c:commit-1" }, { id: "did:ng:c:commit-2" }]; function fakeNg() { return { doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"), sparql_update: mock(async (..._a: unknown[]) => COMMITS), sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })), // A sentinel: makeNg(), if ever used, would `.bind` and call THIS through // the JS Proxy. We assert the primitives call the raw fns above directly. }; } function inject() { const ng = fakeNg(); configure({ ng: ng as any, useShape: (() => {}) as any }); return ng; } test("docCreate calls the real injected ng.doc_create with the exact args", async () => { const ng = inject(); const nuri = await docCreate("sid-1", "Graph", "data:graph", "store", undefined); expect(nuri).toBe("did:ng:o:new-doc"); expect(ng.doc_create).toHaveBeenCalledTimes(1); expect(ng.doc_create.mock.calls[0]).toEqual(["sid-1", "Graph", "data:graph", "store", undefined]); }); test("sparqlUpdate forwards (sessionId, query, anchor) to the real ng.sparql_update", async () => { const ng = inject(); await sparqlUpdate("sid-2", "INSERT DATA { GRAPH {

} }", "did:ng:o:a"); expect(ng.sparql_update).toHaveBeenCalledTimes(1); expect(ng.sparql_update.mock.calls[0]).toEqual([ "sid-2", "INSERT DATA { GRAPH {

} }", "did:ng:o:a", ]); }); test("sparqlUpdate passes anchor=undefined when omitted", async () => { const ng = inject(); await sparqlUpdate("sid-3", "INSERT DATA {}"); expect(ng.sparql_update.mock.calls[0]).toEqual(["sid-3", "INSERT DATA {}", undefined]); }); test("sparqlQuery forwards (sessionId, query, base, anchor) and returns the raw result", async () => { const ng = inject(); const res = await sparqlQuery("sid-4", "SELECT ?e { GRAPH { ?s ?p ?e } }", undefined, "did:ng:o:g"); expect(res).toEqual({ results: { bindings: [] } }); expect(ng.sparql_query).toHaveBeenCalledTimes(1); expect(ng.sparql_query.mock.calls[0]).toEqual([ "sid-4", "SELECT ?e { GRAPH { ?s ?p ?e } }", undefined, "did:ng:o:g", ]); }); // ── the session id: RELAYED, never converted ────────────────────────────────── // Upstream declares it `string | number` (`Session.session_id`, `index.d.ts:266`) and the // broker hands back a NUMBER, which the wasm binding deserializes as a `u64`. Stringifying // it fails that deserialization for real (`Deserialization error of session_id JsValue("1")`), // so what reaches the boundary must be the very value the caller passed — same `typeof`. test("a NUMERIC session id reaches ng untouched, still a number", async () => { const ng = inject(); // Anchor the update and the read on the document just CREATED: creating it mints its // cap, so the reach guard (process-wide once any cap exists) is satisfied the way a real // application satisfies it — rather than by naming a document this user does not hold. const created = await docCreate(1, "Graph", "data:graph", "store", undefined); await sparqlUpdate(2, "INSERT DATA {}", created); await sparqlQuery(3, "SELECT * {}", undefined, created); const createdSid = ng.doc_create.mock.calls[0]![0]; expect(createdSid).toBe(1); expect(typeof createdSid).toBe("number"); const updated = ng.sparql_update.mock.calls[0]![0]; expect(updated).toBe(2); expect(typeof updated).toBe("number"); const queried = ng.sparql_query.mock.calls[0]![0]; expect(queried).toBe(3); expect(typeof queried).toBe("number"); }); test("sparqlUpdate hands back what the boundary returned", async () => { // The commits must arrive at the caller unchanged — the surface used to declare // `Promise` and throw this answer away. inject(); const returned = await sparqlUpdate("sid-c", "INSERT DATA {}", "did:ng:o:a"); expect(returned).toBe(COMMITS); }); test("the primitives do NOT route through the public ng proxy (makeNg)", async () => { // makeNg builds a JS Proxy over the injected ng. If a primitive went through // it, calls would land on the proxy's `get` trap, not on our raw mock fns. // Spy on makeNg: it must never be invoked by the docs primitives. const spy = mock(ngProxy.makeNg); const ng = inject(); await docCreate("sid", "Graph", "data:graph", "store"); await sparqlUpdate("sid", "INSERT DATA {}"); await sparqlQuery("sid", "SELECT * {}"); expect(spy).toHaveBeenCalledTimes(0); // And the raw injected fns were reached directly: expect(ng.doc_create).toHaveBeenCalledTimes(1); expect(ng.sparql_update).toHaveBeenCalledTimes(1); expect(ng.sparql_query).toHaveBeenCalledTimes(1); });