Skip to content
File

Blob: tests/worker/routes/workspaces-member-removal.workers.test.ts

typescript176 lines
1import { and, eq } from "drizzle-orm";
2import { beforeEach, describe, expect, it } from "vitest";
3 
4import { memberships, pageShares } from "@/worker/db/d1/schema";
5import { getDb, resetD1Tables } from "@tests/worker/helpers/db";
6import { apiRequest } from "@tests/worker/helpers/request";
7import { seedMembership, seedPage, seedPageShare, seedUser, seedWorkspace } from "@tests/worker/helpers/seeds";
8 
9describe("DELETE /workspaces/:id/members/:uid", () => {
10 beforeEach(async () => {
11 await resetD1Tables();
12 });
13 
14 it("atomically deletes user-grantee page_shares and the membership row when an admin removes a member", async () => {
15 const admin = await seedUser();
16 const member = await seedUser();
17 const other = await seedUser();
18 const workspace = await seedWorkspace({ owner_id: admin.id, seedOwnerMembership: false });
19 await seedMembership({ user_id: admin.id, workspace_id: workspace.id, role: "admin" });
20 await seedMembership({ user_id: member.id, workspace_id: workspace.id, role: "member" });
21 
22 const page = await seedPage({ workspace_id: workspace.id, created_by: admin.id });
23 const grantToRemovedMember = await seedPageShare({
24 page_id: page.id,
25 created_by: admin.id,
26 grantee_type: "user",
27 grantee_id: member.id,
28 permission: "edit",
29 });
30 const grantToOtherUser = await seedPageShare({
31 page_id: page.id,
32 created_by: admin.id,
33 grantee_type: "user",
34 grantee_id: other.id,
35 permission: "view",
36 });
37 const linkShare = await seedPageShare({
38 page_id: page.id,
39 created_by: admin.id,
40 grantee_type: "link",
41 grantee_id: null,
42 link_token: "share-token-abc",
43 permission: "view",
44 });
45 
46 const res = await apiRequest(`/api/v1/workspaces/${workspace.id}/members/${member.id}`, {
47 method: "DELETE",
48 userId: admin.id,
49 });
50 
51 expect(res.status).toBe(200);
52 await expect(res.json()).resolves.toEqual({ ok: true });
53 
54 const db = getDb();
55 const remainingMembership = await db
56 .select()
57 .from(memberships)
58 .where(and(eq(memberships.user_id, member.id), eq(memberships.workspace_id, workspace.id)))
59 .get();
60 expect(remainingMembership).toBeUndefined();
61 
62 const remainingRemovedMemberShare = await db
63 .select()
64 .from(pageShares)
65 .where(eq(pageShares.id, grantToRemovedMember.id))
66 .get();
67 expect(remainingRemovedMemberShare).toBeUndefined();
68 
69 const remainingOtherShare = await db.select().from(pageShares).where(eq(pageShares.id, grantToOtherUser.id)).get();
70 expect(remainingOtherShare).toBeDefined();
71 
72 const remainingLinkShare = await db.select().from(pageShares).where(eq(pageShares.id, linkShare.id)).get();
73 expect(remainingLinkShare).toBeDefined();
74 });
75 
76 it("refuses to remove the workspace owner", async () => {
77 const owner = await seedUser();
78 const admin = await seedUser();
79 const workspace = await seedWorkspace({ owner_id: owner.id });
80 await seedMembership({ user_id: admin.id, workspace_id: workspace.id, role: "admin" });
81 
82 const res = await apiRequest(`/api/v1/workspaces/${workspace.id}/members/${owner.id}`, {
83 method: "DELETE",
84 userId: admin.id,
85 });
86 
87 expect(res.status).toBe(403);
88 
89 const db = getDb();
90 const ownerMembership = await db
91 .select()
92 .from(memberships)
93 .where(and(eq(memberships.user_id, owner.id), eq(memberships.workspace_id, workspace.id)))
94 .get();
95 expect(ownerMembership?.role).toBe("owner");
96 });
97 
98 it("refuses self-removal from the owner role", async () => {
99 const owner = await seedUser();
100 const workspace = await seedWorkspace({ owner_id: owner.id });
101 
102 const res = await apiRequest(`/api/v1/workspaces/${workspace.id}/members/${owner.id}`, {
103 method: "DELETE",
104 userId: owner.id,
105 });
106 
107 expect(res.status).toBe(403);
108 
109 const db = getDb();
110 const ownerMembership = await db
111 .select()
112 .from(memberships)
113 .where(and(eq(memberships.user_id, owner.id), eq(memberships.workspace_id, workspace.id)))
114 .get();
115 expect(ownerMembership?.role).toBe("owner");
116 });
117 
118 it("allows a guest to leave and clears their user-grantee shares", async () => {
119 const owner = await seedUser();
120 const guest = await seedUser();
121 const workspace = await seedWorkspace({ owner_id: owner.id });
122 await seedMembership({ user_id: guest.id, workspace_id: workspace.id, role: "guest" });
123 
124 const page = await seedPage({ workspace_id: workspace.id, created_by: owner.id });
125 const grantToGuest = await seedPageShare({
126 page_id: page.id,
127 created_by: owner.id,
128 grantee_type: "user",
129 grantee_id: guest.id,
130 permission: "view",
131 });
132 
133 const res = await apiRequest(`/api/v1/workspaces/${workspace.id}/members/${guest.id}`, {
134 method: "DELETE",
135 userId: guest.id,
136 });
137 
138 expect(res.status).toBe(200);
139 
140 const db = getDb();
141 const remainingMembership = await db
142 .select()
143 .from(memberships)
144 .where(and(eq(memberships.user_id, guest.id), eq(memberships.workspace_id, workspace.id)))
145 .get();
146 expect(remainingMembership).toBeUndefined();
147 
148 const remainingShare = await db.select().from(pageShares).where(eq(pageShares.id, grantToGuest.id)).get();
149 expect(remainingShare).toBeUndefined();
150 });
151 
152 it("rejects a non-admin member trying to remove someone else", async () => {
153 const owner = await seedUser();
154 const member = await seedUser();
155 const victim = await seedUser();
156 const workspace = await seedWorkspace({ owner_id: owner.id });
157 await seedMembership({ user_id: member.id, workspace_id: workspace.id, role: "member" });
158 await seedMembership({ user_id: victim.id, workspace_id: workspace.id, role: "member" });
159 
160 const res = await apiRequest(`/api/v1/workspaces/${workspace.id}/members/${victim.id}`, {
161 method: "DELETE",
162 userId: member.id,
163 });
164 
165 expect(res.status).toBe(403);
166 
167 const db = getDb();
168 const victimMembership = await db
169 .select()
170 .from(memberships)
171 .where(and(eq(memberships.user_id, victim.id), eq(memberships.workspace_id, workspace.id)))
172 .get();
173 expect(victimMembership?.role).toBe("member");
174 });
175});