Skip to content
File

Blob: tests/worker/routes/upload-auth-status.workers.test.ts

typescript145 lines
1import { env } from "cloudflare:workers";
2import { beforeEach, describe, expect, it } from "vitest";
3 
4import { apiRequest, PROD_ORIGIN } from "@tests/worker/helpers/request";
5import { resetD1Tables } from "@tests/worker/helpers/db";
6import {
7 seedMembership,
8 seedPage,
9 seedPageShare,
10 seedUpload,
11 seedUser,
12 seedWorkspace,
13} from "@tests/worker/helpers/seeds";
14 
15async function resetR2() {
16 let cursor: string | undefined;
17 do {
18 const list = await env.R2.list({ cursor });
19 if (list.objects.length > 0) {
20 await env.R2.delete(list.objects.map((o) => o.key));
21 }
22 cursor = list.truncated ? list.cursor : undefined;
23 } while (cursor);
24}
25 
26describe("PUT /uploads/:id/data - auth status semantics", () => {
27 beforeEach(async () => {
28 await resetD1Tables();
29 await resetR2();
30 });
31 
32 it("returns 401 unauthorized when no share token and no bearer is provided", async () => {
33 const owner = await seedUser();
34 const ws = await seedWorkspace({ owner_id: owner.id });
35 const page = await seedPage({ workspace_id: ws.id, created_by: owner.id });
36 const upload = await seedUpload({
37 workspace_id: ws.id,
38 page_id: page.id,
39 uploaded_by: owner.id,
40 r2_key: "uploads/no-bearer.png",
41 });
42 
43 const res = await apiRequest(`/uploads/${upload.id}/data`, {
44 method: "PUT",
45 body: new ArrayBuffer(8),
46 headers: { "content-type": "image/png" },
47 origin: PROD_ORIGIN,
48 });
49 
50 expect(res.status).toBe(401);
51 const body = (await res.json()) as { error: string };
52 expect(body.error).toBe("unauthorized");
53 });
54 
55 it("returns 403 forbidden for a valid bearer that does not own the upload row", async () => {
56 const owner = await seedUser();
57 const member = await seedUser();
58 const ws = await seedWorkspace({ owner_id: owner.id });
59 await seedMembership({ user_id: member.id, workspace_id: ws.id, role: "member" });
60 const page = await seedPage({ workspace_id: ws.id, created_by: owner.id });
61 const upload = await seedUpload({
62 workspace_id: ws.id,
63 page_id: page.id,
64 uploaded_by: owner.id,
65 r2_key: "uploads/non-owner.png",
66 });
67 
68 const res = await apiRequest(`/uploads/${upload.id}/data`, {
69 method: "PUT",
70 body: new ArrayBuffer(8),
71 headers: { "content-type": "image/png" },
72 userId: member.id,
73 origin: PROD_ORIGIN,
74 });
75 
76 expect(res.status).toBe(403);
77 const body = (await res.json()) as { error: string };
78 expect(body.error).toBe("forbidden");
79 });
80 
81 it("returns 403 forbidden when a share token does not grant edit", async () => {
82 const owner = await seedUser();
83 const ws = await seedWorkspace({ owner_id: owner.id });
84 const page = await seedPage({ workspace_id: ws.id, created_by: owner.id });
85 await seedPageShare({
86 page_id: page.id,
87 created_by: owner.id,
88 grantee_type: "link",
89 grantee_id: null,
90 link_token: "tok-view",
91 permission: "view",
92 });
93 const upload = await seedUpload({
94 workspace_id: ws.id,
95 page_id: page.id,
96 uploaded_by: owner.id,
97 r2_key: "uploads/share-view.png",
98 });
99 
100 const res = await apiRequest(`/uploads/${upload.id}/data`, {
101 method: "PUT",
102 body: new ArrayBuffer(8),
103 headers: { "content-type": "image/png" },
104 shareToken: "tok-view",
105 origin: PROD_ORIGIN,
106 });
107 
108 expect(res.status).toBe(403);
109 const body = (await res.json()) as { error: string };
110 expect(body.error).toBe("forbidden");
111 });
112 
113 it("returns 200 for a share token with edit permission", async () => {
114 const owner = await seedUser();
115 const ws = await seedWorkspace({ owner_id: owner.id });
116 const page = await seedPage({ workspace_id: ws.id, created_by: owner.id });
117 await seedPageShare({
118 page_id: page.id,
119 created_by: owner.id,
120 grantee_type: "link",
121 grantee_id: null,
122 link_token: "tok-edit",
123 permission: "edit",
124 });
125 const upload = await seedUpload({
126 workspace_id: ws.id,
127 page_id: page.id,
128 uploaded_by: owner.id,
129 r2_key: "uploads/share-edit.png",
130 content_type: "image/png",
131 size_bytes: 8,
132 });
133 
134 const res = await apiRequest(`/uploads/${upload.id}/data`, {
135 method: "PUT",
136 body: new ArrayBuffer(8),
137 headers: { "content-type": "image/png" },
138 shareToken: "tok-edit",
139 origin: PROD_ORIGIN,
140 });
141 
142 expect(res.status).toBe(200);
143 });
144});