51b6278bf6
Its only callers were its own definition and its dedicated test block; no production code used it since the read-union removal (T01.f). The one test that exercised readDocumentSubjects' real integration path (not ensureReposOpen itself) is kept, moved under the ensureRepoOpen describe block it actually belongs to.
185 lines
7.5 KiB
TypeScript
185 lines
7.5 KiB
TypeScript
/**
|
|
* open-repo.test.ts — behavioral tests for ensureRepoOpen (src/open-repo.ts).
|
|
*
|
|
* Core invariant: on a fresh session over a persistent wallet, a scope-index
|
|
* or entity repo is NOT yet in `self.repos`, so an anchored sparql_query returns
|
|
* 0 rows. `ensureRepoOpen(nuri)` calls `doc_subscribe(nuri, …)` FIRST (which
|
|
* pushes the repo into the session), then the anchored read returns data.
|
|
*
|
|
* Fake design:
|
|
* - sparql_query returns EMPTY for a nuri UNTIL doc_subscribe has been called
|
|
* for that nuri (tracked in a Set).
|
|
* - doc_subscribe is a mock that records calls, fires the callback once
|
|
* (simulating the initial State push), then returns an unsubscribe fn.
|
|
*
|
|
* We test ensureRepoOpen via `readDocumentSubjects` (from `surface/read-document`)
|
|
* because that is the production caller — it opens the document before its anchored
|
|
* read, and `emulated-verifier/index-deposit.ts` depends on that ordering to tell an
|
|
* absence from an unsynced repo. It was `readUnion` until `1.0.0-dev.8`; the integration
|
|
* path is the same one, minus the batch.
|
|
*/
|
|
|
|
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
|
|
import { ensureRepoOpen, resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
|
import { readDocumentSubjects } from "../src/surface/read-document";
|
|
import { configure } from "../src/index";
|
|
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
|
|
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
|
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
|
|
|
|
afterAll(() => {
|
|
resetConfig();
|
|
resetStoreRegistry();
|
|
resetRegistryCache();
|
|
resetOpenedRepos();
|
|
});
|
|
|
|
// The reach guard and the cap registry are process-wide: once ANY cap exists the
|
|
// boundary applies to every reader. A suite that declares none must start from an
|
|
// empty one, or it inherits another suite's enforcement.
|
|
beforeEach(() => {
|
|
resetOpenedRepos();
|
|
resetRegistryCache();
|
|
resetCaps();
|
|
setCurrentUser(null);
|
|
});
|
|
|
|
const SESSION = { sessionId: "sid-or", privateStoreId: "PRIV-OR" };
|
|
const TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
|
const FP = "http://festipod.org/";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fake ng builder: tracks which nuris have been doc_subscribe-d.
|
|
// sparql_query returns rows only AFTER the corresponding nuri is subscribed.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeFakeNgWithSubscribe(
|
|
triplesByDoc: Record<string, Array<[string, string]>>,
|
|
) {
|
|
const subscribed = new Set<string>();
|
|
const subscribeCallOrder: string[] = [];
|
|
|
|
// doc_subscribe: record the call, fire callback immediately (initial push), return unsub
|
|
const doc_subscribe = mock(async (nuri: string, _sid: string, cb: (r: unknown) => void) => {
|
|
subscribed.add(nuri);
|
|
subscribeCallOrder.push(nuri);
|
|
// Simulate initial State push (synchronously deferred so the subscription
|
|
// setup promise path in ensureRepoOpen can resolve it).
|
|
setTimeout(() => cb({ V0: { State: {} } }), 0);
|
|
return () => {}; // unsubscribe fn
|
|
});
|
|
|
|
const sparql_query = mock(async (_sid: string, _query: string, _base: unknown, anchor: unknown) => {
|
|
const doc = anchor as string | undefined;
|
|
if (!doc) return { results: { bindings: [] } };
|
|
// Only return data if the repo has been subscribed (i.e. opened)
|
|
if (!subscribed.has(doc)) return { results: { bindings: [] } };
|
|
const triples = triplesByDoc[doc];
|
|
if (!triples) return { results: { bindings: [] } };
|
|
const bindings = triples.map(([p, o]) => ({
|
|
s: { value: doc },
|
|
p: { value: p },
|
|
o: { value: o },
|
|
}));
|
|
return { results: { bindings } };
|
|
});
|
|
|
|
const doc_create = mock(async () => "did:ng:o:new");
|
|
const sparql_update = mock(async () => undefined);
|
|
|
|
return { doc_subscribe, sparql_query, doc_create, sparql_update, subscribed, subscribeCallOrder };
|
|
}
|
|
|
|
function inject(ng: ReturnType<typeof makeFakeNgWithSubscribe>) {
|
|
configure({ ng: ng as any, useShape: (() => {}) as any });
|
|
configureStoreRegistry({
|
|
getSession: async () => SESSION,
|
|
normalizeId: (u: string) => u,
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("ensureRepoOpen", () => {
|
|
it("calls doc_subscribe BEFORE the anchored read returns data", async () => {
|
|
const ng = makeFakeNgWithSubscribe({
|
|
"did:ng:o:a": [[TYPE, `${FP}Event`], [`${FP}title`, "Alpha"]],
|
|
});
|
|
inject(ng);
|
|
|
|
// Directly call ensureRepoOpen then verify read sees data
|
|
await ensureRepoOpen("did:ng:o:a");
|
|
|
|
// doc_subscribe was called for the nuri
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
expect(ng.subscribeCallOrder[0]).toBe("did:ng:o:a");
|
|
|
|
// sparql_query was called AFTER subscribe (ensureRepoOpen guarantees ordering)
|
|
const result = await readDocumentSubjects("did:ng:o:a");
|
|
expect(result.length).toBe(1);
|
|
expect(result[0]!.props[`${FP}title`]).toEqual(["Alpha"]);
|
|
});
|
|
|
|
it("WITHOUT doc_subscribe, sparql_query returns 0 rows (verifies fake mechanics)", async () => {
|
|
const ng = makeFakeNgWithSubscribe({
|
|
"did:ng:o:a": [[TYPE, `${FP}Event`], [`${FP}title`, "Alpha"]],
|
|
});
|
|
inject(ng);
|
|
|
|
// Do NOT call ensureRepoOpen — subscribed Set remains empty
|
|
// Query directly (bypass `readDocumentSubjects`, which opens the repo internally)
|
|
const result = await ng.sparql_query("sid-or", "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, "did:ng:o:a");
|
|
const bindings = (result as any).results.bindings;
|
|
expect(bindings.length).toBe(0); // not subscribed → 0 rows (confirms fake design)
|
|
});
|
|
|
|
it("idempotence: a 2nd ensureRepoOpen for the same nuri does NOT re-subscribe", async () => {
|
|
const ng = makeFakeNgWithSubscribe({
|
|
"did:ng:o:b": [[TYPE, `${FP}Event`]],
|
|
});
|
|
inject(ng);
|
|
|
|
await ensureRepoOpen("did:ng:o:b");
|
|
await ensureRepoOpen("did:ng:o:b"); // second call
|
|
|
|
// doc_subscribe must have been called exactly ONCE
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("no-op when the fake ng has no doc_subscribe (unit fake path)", async () => {
|
|
// Fake ng WITHOUT doc_subscribe
|
|
const noSubscribeNg = {
|
|
doc_create: mock(async () => "did:ng:o:new"),
|
|
sparql_update: mock(async () => undefined),
|
|
sparql_query: mock(async () => ({ results: { bindings: [] } })),
|
|
};
|
|
configure({ ng: noSubscribeNg as any, useShape: (() => {}) as any });
|
|
configureStoreRegistry({
|
|
getSession: async () => SESSION,
|
|
normalizeId: (u: string) => u,
|
|
});
|
|
|
|
// Must not throw; nuri is added to opened Set (guard skips subscribe)
|
|
await expect(ensureRepoOpen("did:ng:o:c")).resolves.toBeUndefined();
|
|
|
|
// Calling again should also be a no-op (idempotent, already in opened)
|
|
await expect(ensureRepoOpen("did:ng:o:c")).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("the anchored document read triggers doc_subscribe then returns data (integration path)", async () => {
|
|
const ng = makeFakeNgWithSubscribe({
|
|
"did:ng:o:p": [[TYPE, `${FP}Participation`], [`${FP}event`, "did:ng:o:e"]],
|
|
});
|
|
inject(ng);
|
|
|
|
const subjects = await readDocumentSubjects("did:ng:o:p");
|
|
|
|
// doc_subscribe was called as part of the `ensureRepoOpen` inside the read
|
|
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
|
expect(subjects.length).toBe(1);
|
|
expect(subjects[0]!.props[`${FP}event`]).toEqual(["did:ng:o:e"]);
|
|
});
|
|
});
|