File
Blob: tests/client/stores/workspace-replica.test.ts
| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 2 | import { createFreshDb, deleteDb } from "@tests/client/util/idb"; |
| 3 | import { createMember, createPage, createWorkspace } from "@tests/client/util/fixtures"; |
| 4 | import type { BlandDatabase } from "@/client/stores/db/bland-db"; |
| 5 | |
| 6 | let db: BlandDatabase; |
| 7 | let replicaCommands: typeof import("@/client/stores/db/workspace-replica").replicaCommands; |
| 8 | |
| 9 | beforeEach(async () => { |
| 10 | vi.resetModules(); |
| 11 | db = createFreshDb(); |
| 12 | vi.doMock("@/client/stores/db/bland-db", async () => { |
| 13 | const actual = await vi.importActual<typeof import("@/client/stores/db/bland-db")>("@/client/stores/db/bland-db"); |
| 14 | return { ...actual, db }; |
| 15 | }); |
| 16 | const mod = await import("@/client/stores/db/workspace-replica"); |
| 17 | replicaCommands = mod.replicaCommands; |
| 18 | }); |
| 19 | |
| 20 | afterEach(async () => { |
| 21 | await deleteDb(db); |
| 22 | vi.restoreAllMocks(); |
| 23 | }); |
| 24 | |
| 25 | describe("replicaCommands.replaceWorkspace", () => { |
| 26 | it("stores the workspace head plus its pages and members in one transaction", async () => { |
| 27 | const workspace = createWorkspace({ id: "ws-1", slug: "team" }); |
| 28 | const pages = [createPage({ id: "p1", workspace_id: "ws-1" })]; |
| 29 | const members = [createMember({ workspace_id: "ws-1" })]; |
| 30 | |
| 31 | await replicaCommands.replaceWorkspace({ |
| 32 | workspace, |
| 33 | accessMode: "member", |
| 34 | workspaceRole: "member", |
| 35 | pages, |
| 36 | members, |
| 37 | }); |
| 38 | |
| 39 | expect(await db.workspaceReplicas.get("ws-1")).toMatchObject({ |
| 40 | id: "ws-1", |
| 41 | slug: "team", |
| 42 | accessMode: "member", |
| 43 | workspaceRole: "member", |
| 44 | }); |
| 45 | expect(await db.workspacePages.toArray()).toEqual(pages); |
| 46 | expect(await db.workspaceMembers.toArray()).toEqual(members); |
| 47 | }); |
| 48 | |
| 49 | it("replaces prior pages and members scoped to the same workspace", async () => { |
| 50 | const workspace = createWorkspace({ id: "ws-1" }); |
| 51 | const page1 = createPage({ id: "p1", workspace_id: "ws-1" }); |
| 52 | const page2 = createPage({ id: "p2", workspace_id: "ws-1" }); |
| 53 | |
| 54 | await replicaCommands.replaceWorkspace({ |
| 55 | workspace, |
| 56 | accessMode: "member", |
| 57 | workspaceRole: "member", |
| 58 | pages: [page1, page2], |
| 59 | members: [], |
| 60 | }); |
| 61 | await replicaCommands.replaceWorkspace({ |
| 62 | workspace, |
| 63 | accessMode: "member", |
| 64 | workspaceRole: "member", |
| 65 | pages: [page2], |
| 66 | members: [], |
| 67 | }); |
| 68 | |
| 69 | const remaining = await db.workspacePages.toArray(); |
| 70 | expect(remaining.map((p) => p.id)).toEqual(["p2"]); |
| 71 | }); |
| 72 | |
| 73 | it("does not disturb pages or members in other workspaces", async () => { |
| 74 | const wsA = createWorkspace({ id: "ws-a" }); |
| 75 | const wsB = createWorkspace({ id: "ws-b" }); |
| 76 | await replicaCommands.replaceWorkspace({ |
| 77 | workspace: wsA, |
| 78 | accessMode: "member", |
| 79 | workspaceRole: "member", |
| 80 | pages: [createPage({ id: "pa", workspace_id: "ws-a" })], |
| 81 | members: [createMember({ workspace_id: "ws-a", user_id: "u-a" })], |
| 82 | }); |
| 83 | await replicaCommands.replaceWorkspace({ |
| 84 | workspace: wsB, |
| 85 | accessMode: "member", |
| 86 | workspaceRole: "member", |
| 87 | pages: [createPage({ id: "pb", workspace_id: "ws-b" })], |
| 88 | members: [createMember({ workspace_id: "ws-b", user_id: "u-b" })], |
| 89 | }); |
| 90 | |
| 91 | await replicaCommands.replaceWorkspace({ |
| 92 | workspace: wsB, |
| 93 | accessMode: "member", |
| 94 | workspaceRole: "member", |
| 95 | pages: [], |
| 96 | members: [], |
| 97 | }); |
| 98 | |
| 99 | expect((await db.workspacePages.toArray()).map((p) => p.id)).toEqual(["pa"]); |
| 100 | expect((await db.workspaceMembers.toArray()).map((m) => m.user_id)).toEqual(["u-a"]); |
| 101 | }); |
| 102 | }); |
| 103 | |
| 104 | describe("replicaCommands.removePages", () => { |
| 105 | it("deletes every returned archived page id without reparenting children and drops matching pageAccess rows", async () => { |
| 106 | const workspace = createWorkspace({ id: "ws-1" }); |
| 107 | const parent = createPage({ id: "parent", workspace_id: "ws-1", parent_id: null }); |
| 108 | const child = createPage({ id: "child", workspace_id: "ws-1", parent_id: "parent" }); |
| 109 | const sibling = createPage({ id: "sibling", workspace_id: "ws-1", parent_id: null }); |
| 110 | |
| 111 | await replicaCommands.replaceWorkspace({ |
| 112 | workspace, |
| 113 | accessMode: "member", |
| 114 | workspaceRole: "member", |
| 115 | pages: [parent, child, sibling], |
| 116 | members: [], |
| 117 | }); |
| 118 | await replicaCommands.upsertPageAccess("parent", "edit"); |
| 119 | await replicaCommands.upsertPageAccess("child", "view"); |
| 120 | |
| 121 | await replicaCommands.removePages("ws-1", ["parent", "child"]); |
| 122 | |
| 123 | const remaining = await db.workspacePages.toArray(); |
| 124 | expect(remaining.find((p) => p.id === "parent")).toBeUndefined(); |
| 125 | expect(remaining.find((p) => p.id === "child")).toBeUndefined(); |
| 126 | expect(remaining.find((p) => p.id === "sibling")?.parent_id).toBeNull(); |
| 127 | expect(await db.pageAccess.get("parent")).toBeUndefined(); |
| 128 | expect(await db.pageAccess.get("child")).toBeUndefined(); |
| 129 | }); |
| 130 | }); |
| 131 | |
| 132 | describe("replicaCommands.upsertPages", () => { |
| 133 | it("upserts restored page rows returned by the restore route", async () => { |
| 134 | const workspace = createWorkspace({ id: "ws-1" }); |
| 135 | const restored = createPage({ id: "restored", workspace_id: "ws-1", title: "Restored" }); |
| 136 | |
| 137 | await replicaCommands.replaceWorkspace({ |
| 138 | workspace, |
| 139 | accessMode: "member", |
| 140 | workspaceRole: "member", |
| 141 | pages: [], |
| 142 | members: [], |
| 143 | }); |
| 144 | |
| 145 | await replicaCommands.upsertPages("ws-1", [restored]); |
| 146 | |
| 147 | expect(await db.workspacePages.get("restored")).toEqual(restored); |
| 148 | }); |
| 149 | }); |
| 150 | |
| 151 | describe("replicaCommands.patchWorkspaceHead", () => { |
| 152 | it("merges plain Workspace updates without dropping viewer-scoped role / access", async () => { |
| 153 | const workspace = createWorkspace({ id: "ws-1", name: "Old", slug: "old-slug" }); |
| 154 | await replicaCommands.replaceWorkspace({ |
| 155 | workspace, |
| 156 | accessMode: "member", |
| 157 | workspaceRole: "guest", |
| 158 | pages: [], |
| 159 | members: [], |
| 160 | }); |
| 161 | |
| 162 | await replicaCommands.patchWorkspaceHead("ws-1", { name: "New", slug: "new-slug" }); |
| 163 | |
| 164 | const replica = await db.workspaceReplicas.get("ws-1"); |
| 165 | expect(replica?.workspace.name).toBe("New"); |
| 166 | expect(replica?.workspace.slug).toBe("new-slug"); |
| 167 | expect(replica?.slug).toBe("new-slug"); |
| 168 | expect(replica?.workspaceRole).toBe("guest"); |
| 169 | expect(replica?.accessMode).toBe("member"); |
| 170 | }); |
| 171 | }); |
| 172 | |
| 173 | describe("replicaCommands.removePage", () => { |
| 174 | it("removes the page row and any matching pageAccess row", async () => { |
| 175 | await replicaCommands.replaceWorkspace({ |
| 176 | workspace: createWorkspace({ id: "ws-1" }), |
| 177 | accessMode: "member", |
| 178 | workspaceRole: "member", |
| 179 | pages: [createPage({ id: "p1", workspace_id: "ws-1" })], |
| 180 | members: [], |
| 181 | }); |
| 182 | await replicaCommands.upsertPageAccess("p1", "view"); |
| 183 | |
| 184 | await replicaCommands.removePage("ws-1", "p1"); |
| 185 | |
| 186 | expect(await db.workspacePages.get("p1")).toBeUndefined(); |
| 187 | expect(await db.pageAccess.get("p1")).toBeUndefined(); |
| 188 | }); |
| 189 | }); |
| 190 | |
| 191 | describe("replicaCommands.upsertPageAccess", () => { |
| 192 | it("writes and overwrites the mode for a page id", async () => { |
| 193 | await replicaCommands.upsertPageAccess("p1", "view"); |
| 194 | expect((await db.pageAccess.get("p1"))?.mode).toBe("view"); |
| 195 | await replicaCommands.upsertPageAccess("p1", "edit"); |
| 196 | expect((await db.pageAccess.get("p1"))?.mode).toBe("edit"); |
| 197 | }); |
| 198 | }); |