Files

153 lines
6.6 KiB
TypeScript

import { expect, test } from "bun:test";
import { indexingOn, type Indexing } from "../src/indexing";
import type { Nuri } from "../src/port";
import { INDEX_FIELD } from "../src/vocabulary";
import { FakeNextGraph, publishObject } from "./fake-nextgraph";
/**
* The two acts, and nothing else — this package creates an index and hands one a
* reference. What becomes of that reference is the business of the layer that
* processes the index's inbox, and there is nothing here that could do it or ask
* for it.
*
* Each actor gets their own handle, and they share no variable carrying business
* data. The ONE value that crosses between them is the index's NURI — and that
* crossing is the mechanism this design names: an application references the
* index's NURI in its own source. `hardcodedInAppSource` marks every such
* crossing, so anything else moving between actors would stand out.
*/
const PUBLISHED_AT = "http://schema.org/datePublished";
const NAME = "http://schema.org/name";
function hardcodedInAppSource(nuri: Nuri): Nuri {
return nuri;
}
type Port = ReturnType<FakeNextGraph["portFor"]>;
function world(): {
network: FakeNextGraph;
alice: Indexing;
bob: Indexing;
ports: { alice: Port; bob: Port };
} {
const network = new FakeNextGraph();
const ports = { alice: network.portFor("alice"), bob: network.portFor("bob") };
// A handle is nothing but a port bound to one identity: building one reaches
// nothing, which is why there is nothing to await here.
return { network, alice: indexingOn(ports.alice), bob: indexingOn(ports.bob), ports };
}
// --- creating an index ----------------------------------------------------
test("any user creates an index in their public store, and it declares its field", async () => {
const { alice, network } = world();
const index = await alice.create(PUBLISHED_AT);
// An ordinary document: what makes it an index is the field it declares, on the
// index's own subject, which is where a reader of the document finds it.
expect(network.contentsOf(index)).toEqual([
{ subject: index, predicate: INDEX_FIELD, values: [PUBLISHED_AT] },
]);
});
test("a new index is ready to receive: its inbox is open the moment create returns", async () => {
const { alice, bob, network } = world();
const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT));
// Nothing to open, register or remember — a stranger deposits straight away.
await bob.add(index, "did:ng:o:doc-9");
expect(network.depositsIn(index)).toHaveLength(1);
});
test("a field that could never match an object is refused at creation", async () => {
const { alice } = world();
// It cannot be corrected later — nothing here deletes — so it is refused now.
await expect(alice.create("")).rejects.toThrow(/cannot be changed later/);
await expect(alice.create(" ")).rejects.toThrow(/cannot be changed later/);
});
test("two indexes are two documents, each declaring its own field", async () => {
const { alice, network } = world();
const byDate = await alice.create(PUBLISHED_AT);
const byName = await alice.create(NAME);
expect(byDate).not.toBe(byName);
expect(network.contentsOf(byDate)).toEqual([
{ subject: byDate, predicate: INDEX_FIELD, values: [PUBLISHED_AT] },
]);
expect(network.contentsOf(byName)).toEqual([
{ subject: byName, predicate: INDEX_FIELD, values: [NAME] },
]);
});
// --- handing an index a reference -----------------------------------------
test("a stranger hands an index a reference, and it waits in the index's inbox", async () => {
const { alice, bob, ports, network } = world();
// Alice creates the index and its NURI goes into the application's source.
const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT));
// Bob, who owns nothing of Alice's, creates his own public object and hands the
// index a reference to it. He needs no permission and gets no write.
const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04");
await bob.add(index, article);
// THE WHOLE PAYLOAD is the reference: no wrapper, no claim, no copy of the value,
// and no index either — the inbox it landed in is what identifies one. This is the
// one thing this package hands the layer that will make an entry of it.
expect(network.depositsIn(index)).toEqual([
{ from: "bob", payload: article, ts: expect.any(Number) },
]);
// And it stayed a deposit: nothing wrote it into the index document.
expect(network.contentsOf(index)).toEqual([
{ subject: index, predicate: INDEX_FIELD, values: [PUBLISHED_AT] },
]);
});
test("the same reference handed over twice is two deposits, and nothing is lost", async () => {
const { alice, bob, ports, network } = world();
const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT));
const article = await publishObject(ports.bob, PUBLISHED_AT, "2026-03-04");
await bob.add(index, article);
await bob.add(index, article);
// Nothing here de-duplicates: a deposit is an invitation to look, so repeating one
// is legitimate and cheap, and whoever processes the inbox is the one that settles.
expect(network.depositsIn(index)?.map((d) => d.payload)).toEqual([article, article]);
});
test("an index whose owner never opened an inbox refuses a deposit rather than losing it", async () => {
const { bob, ports } = world();
// A public document that was never made into an index: no inbox was opened.
const notAnIndex = hardcodedInAppSource(await ports.alice.createPublicDocument());
await expect(bob.add(notAnIndex, "did:ng:o:doc-9")).rejects.toThrow(/has no inbox/);
});
test("a payload that is not a reference is refused here, not deposited for someone else to find", async () => {
const { alice, bob } = world();
const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT));
// Both sides are checked, because both go on to name a document. An index's inbox
// takes anything anyone posts to it; what THIS package puts there is a NURI.
await expect(bob.add(index, "please index my article")).rejects.toThrow(/not a NURI/);
await expect(bob.add("http://example.org/index", "did:ng:o:doc-9")).rejects.toThrow(
/not a NURI/,
);
});
// --- only the owner owns it -----------------------------------------------
test("nobody but the owner writes an index, whatever they know about it", async () => {
const { alice, ports } = world();
const index = hardcodedInAppSource(await alice.create(PUBLISHED_AT));
await expect(
ports.bob.addLiteralProperty(index, "did:ng:o:forged", INDEX_FIELD, "http://schema.org/aaa"),
).rejects.toThrow(/only a document's owner writes to it/);
await expect(ports.bob.openInbox(index)).rejects.toThrow(/may not open an inbox/);
});