clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/prf-provision-custody
lofi / package / access / access_sync_test.ts
access_sync_test.ts 4.7K
1import { createDb, schema as s } from "jazz-tools";2import { deploy, startLocalJazzServer } from "jazz-tools/testing";3import {4 defineAccessPolicies,5 groupAccess,6 groupMembershipTable,7 groupRoleCapabilities,8 sharedAccess,9 sharedGrantTable,10} from "./mod.ts";11import { assert } from "../runtime/test-assert.ts";1213const app = s.defineApp({14 sharedDocs: s.table({ title: s.string() }),15 sharedDocGrants: sharedGrantTable("sharedDocs"),16 workspaces: s.table({ name: s.string() }),17 workspaceMembers: groupMembershipTable("workspaces"),18 workspaceDocs: s.table({ workspaceId: s.ref("workspaces"), title: s.string() }),19});20const permissions = defineAccessPolicies(app, [21 sharedAccess({ resource: app.sharedDocs, grants: app.sharedDocGrants }),22 groupAccess({23 groups: app.workspaces,24 members: app.workspaceMembers,25 resources: app.workspaceDocs,26 groupId: "workspaceId",27 }),28]);2930function secret(fill: number): string {31 let binary = "";32 for (const byte of new Uint8Array(32).fill(fill)) binary += String.fromCharCode(byte);33 return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");34}3536async function within<T>(operation: Promise<T>, label: string, milliseconds = 10_000): Promise<T> {37 let timer: ReturnType<typeof setTimeout> | undefined;38 try {39 return await Promise.race([40 operation,41 new Promise<never>((_resolve, reject) => {42 timer = setTimeout(() => reject(new Error(`${label} timed out`)), milliseconds);43 }),44 ]);45 } finally {46 if (timer !== undefined) clearTimeout(timer);47 }48}4950Deno.test("real sync persists offline grant, revoke, and membership changes after reconnect", async () => {51 const server = await startLocalJazzServer({ allowLocalFirstAuth: true });52 await deploy({53 appId: server.appId,54 serverUrl: server.url,55 adminSecret: server.adminSecret,56 schema: app,57 permissions,58 });59 const alice = await createDb({60 appId: server.appId,61 serverUrl: server.url,62 secret: secret(1),63 userBranch: "main",64 driver: { type: "memory" },65 });66 const bob = await createDb({67 appId: server.appId,68 serverUrl: server.url,69 secret: secret(2),70 userBranch: "main",71 driver: { type: "memory" },72 });73 try {74 const aliceId = alice.getAuthState().session?.user_id;75 const bobId = bob.getAuthState().session?.user_id;76 assert(Boolean(aliceId && bobId && aliceId !== bobId), "test principals were not isolated");7778 const shared = await alice.insert(app.sharedDocs, { title: "shared" }).wait({ tier: "global" });79 await alice.disconnect();80 const grant = alice.insert(app.sharedDocGrants, {81 resourceId: shared.id,82 user_id: bobId!,83 can_edit: false,84 });85 assert(grant.value.user_id === bobId, "offline grant did not apply locally");86 await alice.reconnect();87 await within(grant.wait({ tier: "global" }), "grant global durability");88 assert(89 (await alice.all(app.sharedDocGrants.where({ id: grant.value.id }), { tier: "global" }))90 .length === 1,91 "reconnected grant did not persist globally",92 );9394 await alice.disconnect();95 const revoke = alice.delete(app.sharedDocGrants, grant.value.id);96 await alice.reconnect();97 await within(revoke.wait({ tier: "global" }), "revoke global durability");98 assert(99 (await alice.all(app.sharedDocGrants.where({ id: grant.value.id }), { tier: "global" }))100 .length === 0,101 "reconnected revoke did not persist globally",102 );103104 const workspace = await alice.insert(app.workspaces, {105 name: "workspace",106 }).wait({ tier: "global" });107 const initialAdmin = await alice.insert(app.workspaceMembers, {108 groupId: workspace.id,109 user_id: aliceId!,110 ...groupRoleCapabilities("admin"),111 }).wait({ tier: "global" });112 assert(113 initialAdmin.role === "admin",114 "group bootstrap did not create the first admin membership",115 );116 await alice.disconnect();117 const membership = alice.insert(app.workspaceMembers, {118 groupId: workspace.id,119 user_id: bobId!,120 ...groupRoleCapabilities("reader"),121 });122 assert(membership.value.user_id === bobId, "offline membership did not apply locally");123 await alice.reconnect();124 await within(membership.wait({ tier: "global" }), "membership global durability");125 assert(126 (await alice.all(app.workspaceMembers.where({ id: membership.value.id }), { tier: "global" }))127 .length === 1,128 "reconnected membership did not persist globally",129 );130 } finally {131 await Promise.allSettled([132 within(alice.logout(), "Alice cleanup", 3_000),133 within(bob.logout(), "Bob cleanup", 3_000),134 ]);135 await within(server.stop(), "server cleanup", 3_000).catch(() => undefined);136 }137});