153 lines
6.6 KiB
TypeScript
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/);
|
|
});
|