/** * The real FabriChat manager, creating rooms in spaces of their own, each its * space's root, in a space that declares itself a `fabrichat-room`. Which * space a room lives in, what that space's root is, and what kind the space * declares are things a pattern can't read, so they are checked here, against * a runtime and storage of the test's own; `../fabrichat/creation.test.tsx` * covers the rest of what the manager does with the rooms it creates. */ import { expect } from "@std/expect"; import { fromFileUrl } from "@std/path"; import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { Identity } from "@commonfabric/identity"; import { aclDocId } from "@commonfabric/memory/acl"; import { type Cell, inSpaceRootCause, isCell, Runtime, } from "@commonfabric/runner"; import { markRendererTrustedEvent, reviewedActionProvenance, } from "@commonfabric/runner/cfc"; import { setCfcImplementationIdentity } from "@commonfabric/runner/cfc/trust-authority"; import { resolveLocalProgram } from "@commonfabric/runner/local-program.deno"; import { EmulatedStorageManager, newLoopbackServer, } from "@commonfabric/runner/storage/cache.deno"; const signer = await Identity.fromPassphrase("fabrichat-manager"); const home = signer.did(); // Stand-ins for principals, each a base58btc key as a principal's is. const BOB = "did:key:z6MkBob"; const CAROL = "did:key:z6MkCaro1"; const MANAGER_PATH = fromFileUrl( new URL("../fabrichat/manager.tsx", import.meta.url), ); // The patterns package, which the manager's imports reach across. const PATTERNS = fromFileUrl(new URL("..", import.meta.url)); const RESULT_CAUSE = "fabrichat manager"; // The reviewed action a start is admitted from, as // `../fabrichat/schemas.tsx` names it. const START_ACTION = { surface: "ChatStartSurface", action: "ChatStart" }; /** * `event` as a click on the manager's start control delivers it: carrying the * reviewed action, and marked as the renderer marks a click it delivers. */ const startClick = ( event: Record, ): Record => { const click = { type: "click", ...event, provenance: reviewedActionProvenance("dom", START_ACTION), }; markRendererTrustedEvent(click); return click; }; // A stand-in for this user's `#profile`, labeled, as a Fabric profile is, // because a room's participants link only a document that carries a label. const profileSchema = { type: "object", properties: { name: { type: "string" } }, ifc: { addIntegrity: ["fabrichat-test-profile"] }, // deno-lint-ignore no-explicit-any } as any; // The writer that stands in for a profile's own handlers, which a // `represents-principal` claim needs. const PROFILE_WRITER = "fabrichat-manager-test-profile"; // A profile labeled as representing the principal that writes it, as a // Fabric profile is. const ownProfileSchema = { type: "object", properties: { name: { type: "string" } }, ifc: { addIntegrity: [{ kind: "represents-principal", subject: { __ctCurrentPrincipal: true }, }], ownerPrincipal: { __ctCurrentPrincipal: true }, writeAuthorizedBy: [PROFILE_WRITER], }, // deno-lint-ignore no-explicit-any } as any; // Reads a room's participants as links, not copies. const participantListSchema = { type: "array", items: { type: "unknown", asCell: ["cell"] }, // deno-lint-ignore no-explicit-any } as any; // Reads an index entry with its room as a link, not a copy. const entryListSchema = { type: "array", items: { type: "object", properties: { room: { type: "unknown", asCell: ["cell"] }, kind: { type: "string" }, counterpart: { type: "string" }, }, }, // deno-lint-ignore no-explicit-any } as any; describe("fabrichat-manager", () => { let server: ReturnType; let storageManager: EmulatedStorageManager; let runtime: Runtime; beforeEach(() => { server = newLoopbackServer(); storageManager = EmulatedStorageManager.connectTo(server, { as: signer }); runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); }); afterEach(async () => { // Let the rooms the tests created finish starting before the replicas // close under them. await runtime?.idle(); await storageManager?.synced(); await runtime?.dispose(); await storageManager?.close(); await server?.close(); }); // Starts the manager, and returns a way to send it an event and wait for // the event's effect. const startManager = async () => { // The manager's core, given a profile of its own: `#profile` resolves // nothing here, and a manager starts no chat without one. const program = { ...await resolveLocalProgram( (resolver) => runtime.harness.resolve(resolver), { main: MANAGER_PATH, root: PATTERNS }, ), mainExport: "FabriChatManagerCore", }; const tx = runtime.edit(); const pattern = await runtime.patternManager.compilePattern(program, { space: home, tx, }); const profile = runtime.getCell<{ name: string }>( home, "profile", profileSchema, tx, ); profile.set({ name: "Tester" }); const resultCell = runtime.getCell>( home, RESULT_CAUSE, undefined, tx, ); const manager = runtime.run( tx, // deno-lint-ignore no-explicit-any pattern as any, { myProfile: profile }, resultCell, ); runtime.prepareTxForCommit(tx); expect((await tx.commit().settled).error).toBeUndefined(); await manager.pull(); const send = async (stream: string, event: Record) => { const sendTx = runtime.edit(); manager.withTx(sendTx).key(stream).send( stream === "openDirect" || stream === "createGroup" ? startClick(event) : event, ); expect((await sendTx.commit().settled).error).toBeUndefined(); await runtime.idle(); await manager.pull(); }; const rooms = () => // deno-lint-ignore no-explicit-any manager.key("rooms").asSchema(entryListSchema).get() as any[]; return { manager, send, rooms, profile }; }; it("reads a chip's `target.name`, arriving as the link to a profile, as that profile's cell", async () => { const { send, manager } = await startManager(); // The test's own profile, labeled as representing the principal the // manager runs for. const tx = runtime.edit(); setCfcImplementationIdentity(tx, { kind: "builtin", builtinId: PROFILE_WRITER, }); const own = runtime.getCell(home, "own profile", ownProfileSchema, tx); own.set({ name: "Tester" }); expect((await tx.commit().settled).error).toBeUndefined(); const link = own.toSigilLinkOrNull(); if (link === null) throw new Error("The profile has no link."); // A bound target property crosses from the page as the link to its cell, // which is what the worker hands the stream; text in its place names no // one. await send("openDirect", { requestId: "d-linked", target: { name: link } }); await send("openDirect", { requestId: "d-text", target: { name: "Tester" }, }); const requests = manager.key("requests").get() as Record< string, unknown >; expect(requests["d-linked"]).toEqual({ status: "refused", reason: "The counterpart is this user.", }); expect(requests["d-text"]).toEqual({ status: "refused", reason: "The counterpart is not a principal.", }); }); it("creates each room as its space's root, at the address the space's genesis reserves for one", async () => { const { send, rooms } = await startManager(); await send("openDirect", { requestId: "d-1", counterpart: BOB }); await send("createGroup", { requestId: "g-1", title: "Team", members: [CAROL], }); const listed = rooms(); expect(listed.length).toBe(2); for (const entry of listed) { const room = entry.room.resolveAsCell(); const space = room.getAsNormalizedFullLink().space; // Read as a host vetting an offer of the room reads it: the space's // root, as its space cell links it. const root = await runtime.getSpaceCell(space).key("defaultPattern") .pull(); if (!isCell(root)) throw new Error("The room's space has no root."); expect(root.space).toBe(space); expect(root.getAsNormalizedFullLink().path).toEqual([]); expect(root.equalLinks(runtime.getCell(space, inSpaceRootCause(space)))) .toBe(true); expect(root.equalLinks(room)).toBe(true); } }); it("lists this user among the participants of each room it creates", async () => { const { send, rooms, profile } = await startManager(); await send("openDirect", { requestId: "d-1", counterpart: BOB }); await send("createGroup", { requestId: "g-1", title: "Team", members: [CAROL], }); const listed = rooms(); expect(listed.length).toBe(2); for (const entry of listed) { const room = entry.room.resolveAsCell(); await room.pull(); const participants = room.key("participants") .asSchema(participantListSchema).get() as Cell[]; expect(participants.length).toBe(1); expect(participants[0].equalLinks(profile)).toBe(true); } }); it("declares each room's space a `fabrichat-room`", async () => { const { send, rooms } = await startManager(); await send("openDirect", { requestId: "d-1", counterpart: BOB }); await send("createGroup", { requestId: "g-1", title: "Team", members: [CAROL], }); const spaces = rooms().map((entry) => entry.room.getAsNormalizedFullLink().space ); expect(spaces.length).toBe(2); for (const space of spaces) { expect(await runtime.spaceKind(space)).toBe("fabrichat-room"); } }); it("creates each room in a space of its own that grants its members alone", async () => { const { send, rooms } = await startManager(); await send("openDirect", { requestId: "d-1", counterpart: BOB }); await send("createGroup", { requestId: "g-1", title: "Team", members: [CAROL], }); const spaces = rooms().map((entry) => entry.room.getAsNormalizedFullLink().space ); expect(spaces.length).toBe(2); expect(spaces).not.toContain(home); expect(spaces[0]).not.toBe(spaces[1]); // This user and each other member hold OWNER, and no one else holds // anything. const aclOf = async (space: string) => (await server.readDocument( space as Parameters[0], aclDocId(space) as Parameters[1], ))?.value; const spaceOf = (kind: string) => rooms().find((entry) => entry.kind === kind).room .getAsNormalizedFullLink().space; expect(await aclOf(spaceOf("direct"))).toEqual({ [home]: "OWNER", [BOB]: "OWNER", }); expect(await aclOf(spaceOf("group"))).toEqual({ [home]: "OWNER", [CAROL]: "OWNER", }); }); it("grants everyone WRITE on a group room made joinable by its link", async () => { const { send, rooms } = await startManager(); await send("createGroup", { requestId: "g-1", title: "Open team", members: [CAROL], joinableByLink: true, }); const space = rooms()[0].room.getAsNormalizedFullLink().space; expect( (await server.readDocument( space as Parameters[0], aclDocId(space) as Parameters[1], ))?.value, ).toEqual({ [home]: "OWNER", [CAROL]: "OWNER", "*": "WRITE", }); }); });