Files
ng-eventually/packages/polyfill/test/public-store.test.ts
T

189 lines
7.3 KiB
TypeScript

/**
* public-store.test.ts — the emulated *"downloaded from the outerOverlay"*, in isolation.
*
* `cross-user-access.test.ts` proves the consequence end to end (Bob reads Alice's
* public document from a bare reference). This file pins the primitive itself: what it
* asks, what it refuses, and when it says nothing at all.
*/
import { test, expect, mock, afterEach } from "bun:test";
import { exposeReadCap, fetchReadCap, resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
import { mintCap } from "../src/emulated-verifier/caps";
import { getCaps } from "../src/shared-wallet/bootstrap";
import { configure } from "../src/index";
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
import type { Nuri } from "../src/model/types";
const SHIM = "urn:ng-eventually:shim";
const SESSION = { sessionId: "sid-ps", privateStoreId: "PRIV-PS" };
interface Quad { g: string; s: string; p: string; o: string }
/** A fake `ng` holding just enough to answer the Header-branch `exposedReadCap` query. */
function inject() {
const quads: Quad[] = [];
const sparql_update = mock(async (...a: unknown[]) => {
const query = a[1] as string;
const anchor = a[2] as string;
if (/^\s*DELETE WHERE/.test(query)) {
for (let i = quads.length - 1; i >= 0; i--) if (quads[i]!.g === anchor) quads.splice(i, 1);
return undefined;
}
const m = query.match(/<([^>]+)>\s+<([^>]+)>\s+"([^"]*)"/);
if (m) quads.push({ g: anchor, s: m[1]!, p: m[2]!, o: m[3]! });
return undefined;
});
const sparql_query = mock(async (...a: unknown[]) => ({
results: {
bindings: quads
.filter((q) => q.g === (a[3] as string) && q.p === `${SHIM}:exposedReadCap`)
.map((q) => ({ c: { value: q.o } })),
},
}));
configure({ ng: { doc_create: mock(async () => "did:ng:o:x"), sparql_update, sparql_query } as any, useShape: (() => {}) as any });
configureStoreRegistry({ getSession: async () => SESSION });
resetCaps();
resetPublicStoreFetches();
setCurrentUser(null);
return { sparql_query, quads };
}
afterEach(() => {
resetConfig();
resetStoreRegistry();
resetCaps();
setCurrentUser(null);
});
/**
* Alice creates her note and exposes its cap — the two halves of what `createEntityDoc`
* does for a `public` scope, in that order.
*
* The `mint` is not decoration: exposing writes to the document, and writing needs to
* reach it. Without it this file only passed while the emulation happened to be
* DISARMED, which made its results depend on which test file ran first — it went red in
* `bun test <other-file> test/public-store.test.ts`. A fixture that exposes a cap for a
* document nobody holds describes a state the library never produces.
*/
async function aliceExposesHerNote(): Promise<void> {
getCaps().mint(PUB);
await exposeReadCap(PUB, mintCap(PUB));
}
/** Arm the emulation without giving the current holder anything: some OTHER document. */
function armEmulation(): void {
setCurrentUser("someone-else");
getCaps().mint("did:ng:o:unrelated");
}
const PUB = "did:ng:o:pub" as Nuri;
test("a cap exposed on a document is downloaded by a holder that has nothing", async () => {
inject();
setCurrentUser("alice");
await aliceExposesHerNote();
setCurrentUser("bob");
armEmulation();
setCurrentUser("bob");
expect(getCaps().capFor(PUB)).toBeUndefined();
expect(await fetchReadCap(PUB)).toBe(true);
expect(getCaps().capFor(PUB)).toBe(mintCap(PUB));
expect(getCaps().isInPublicStore(PUB)).toBe(true);
});
test("a document that exposes nothing yields nothing — that is the normal case, not an error", async () => {
inject();
armEmulation();
setCurrentUser("bob");
expect(await fetchReadCap("did:ng:o:protected" as Nuri)).toBe(false);
expect(getCaps().capFor("did:ng:o:protected" as Nuri)).toBeUndefined();
});
// A document speaks for itself and for nothing else. Without this, whoever can write
// into one public document could file caps for every document they care to name.
test("a cap naming ANOTHER document is refused, not filed", async () => {
const { quads } = inject();
setCurrentUser("alice");
await aliceExposesHerNote();
// Forge the exposed value so it names a different document.
quads[0]!.o = mintCap("did:ng:o:someone-elses" as Nuri);
armEmulation();
setCurrentUser("bob");
expect(await fetchReadCap(PUB)).toBe(false);
expect(getCaps().capFor(PUB)).toBeUndefined();
expect(getCaps().capFor("did:ng:o:someone-elses" as Nuri)).toBeUndefined();
});
test("inert while no cap has been issued at all — nothing to obtain, nothing asked", async () => {
const { sparql_query } = inject();
setCurrentUser("bob");
expect(await fetchReadCap(PUB)).toBe(false);
expect(sparql_query).toHaveBeenCalledTimes(0);
});
// REGRESSION (2026-08-07, found adversarially). The memo used to cache a BOOLEAN, so the
// first holder to ask triggered the download, the cap was filed for THEM, and every later
// holder got `true` while holding nothing — their next read was refused. Upstream a broker
// serving a pinned outer overlay answers EVERY asker.
test("a public store serves every asker, not only the first", async () => {
inject();
setCurrentUser("alice");
await aliceExposesHerNote();
armEmulation();
setCurrentUser("bob");
expect(await fetchReadCap(PUB)).toBe(true);
expect(getCaps().capFor(PUB)).toBe(mintCap(PUB));
setCurrentUser("carol");
expect(await fetchReadCap(PUB)).toBe(true);
expect(getCaps().capFor(PUB)).toBe(mintCap(PUB)); // …and she HOLDS it, not just "true"
});
test("asked once per document: the outcome is memoised, in both directions", async () => {
const { sparql_query } = inject();
// Counted PER DOCUMENT (the read is anchored on the one being asked about), not over
// every read the process makes. `setCurrentUser` fires the connection work, which reads
// on its own account in the background — none of it about this document — so a total
// makes the memo's arithmetic depend on whatever else happens to be in flight. This is
// the same assertion, about the read it is actually about.
const asks = (nuri: string): number =>
sparql_query.mock.calls.filter((c) => c[3] === nuri).length;
setCurrentUser("alice");
await aliceExposesHerNote();
armEmulation();
setCurrentUser("bob");
await fetchReadCap(PUB);
const afterHit = asks(PUB);
await fetchReadCap(PUB); // held now → not even the memo is consulted
expect(asks(PUB)).toBe(afterHit);
const absent = "did:ng:o:nothing-here" as Nuri;
await fetchReadCap(absent);
const afterMiss = asks(absent);
expect(afterMiss).toBe(1); // it WAS asked once — a memo over nothing proves nothing
await fetchReadCap(absent); // a miss is remembered too
expect(asks(absent)).toBe(afterMiss);
});
test("resetting the caps forgets the memo — a stale yes would hand back what is no longer held", async () => {
const { sparql_query } = inject();
setCurrentUser("alice");
await aliceExposesHerNote();
armEmulation();
setCurrentUser("bob");
await fetchReadCap(PUB);
resetCaps(); // also calls resetPublicStoreFetches
armEmulation();
setCurrentUser("bob");
const before = sparql_query.mock.calls.length;
expect(await fetchReadCap(PUB)).toBe(true);
expect(sparql_query.mock.calls.length).toBeGreaterThan(before); // asked again
});