Skip to content
File

Blob: tests/client/stores/workspace-replica.test.ts

typescript199 lines
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2import { createFreshDb, deleteDb } from "@tests/client/util/idb";
3import { createMember, createPage, createWorkspace } from "@tests/client/util/fixtures";
4import type { BlandDatabase } from "@/client/stores/db/bland-db";
5 
6let db: BlandDatabase;
7let replicaCommands: typeof import("@/client/stores/db/workspace-replica").replicaCommands;
8 
9beforeEach(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 
20afterEach(async () => {
21 await deleteDb(db);
22 vi.restoreAllMocks();
23});
24 
25describe("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 
104describe("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 
132describe("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 
151describe("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 
173describe("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 
191describe("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});