1
18import { test, expect } from "../fixtures";
19import { createPage } from "../utils/api";
20
21async function makePageWithContent(
22 api: import("@playwright/test").APIRequestContext,
23 workspaceId: string,
24 title: string,
25) {
26 const page = await createPage(api, workspaceId, { title });
27 await api.post(`/api/v1/pages/${page.id}/blocks`, {
28 data: { type: "PARAGRAPH", content: { text: "Shared content: the secret is 42." } },
29 });
30 return page;
31}
32
33test.describe("Sharing — Phase 10", () => {
34 test("publish page → public link renders without auth", async ({
35 browser,
36 api,
37 freshWorkspace,
38 }) => {
39 const ws = await freshWorkspace("Share Space");
40 const page = await makePageWithContent(api, ws.id, "Public Report");
41
42
43 const shareRes = await api.post(`/api/v1/pages/${page.id}/share`, {
44 data: { scope: "public" },
45 });
46 expect(shareRes.status()).toBe(201);
47 const share = (await shareRes.json()) as { share: { token: string; url: string } };
48 expect(share.share.token).toBeTruthy();
49
50
51 const anon = await browser.newContext();
52 const anonPage = await anon.newPage();
53 const publicUrl =
54 share.share.url ?? `http://localhost:3000/s/${share.share.token}`;
55 await anonPage.goto(publicUrl, { waitUntil: "domcontentloaded" });
56 await expect(anonPage.getByTestId("public-page")).toBeVisible({ timeout: 20_000 });
57 await expect(anonPage.getByText("the secret is 42")).toBeVisible();
58
59 await expect(anonPage.getByTestId("sidebar")).toHaveCount(0);
60 await expect(anonPage.getByTestId("user-menu")).toHaveCount(0);
61 await anon.close();
62 });
63
64 test("password-protected share: wrong password blocked, correct passes", async ({
65 browser,
66 api,
67 freshWorkspace,
68 }) => {
69 const ws = await freshWorkspace("Password Space");
70 const page = await makePageWithContent(api, ws.id, "Locked Report");
71 const password = "unlock-me-42";
72
73 const shareRes = await api.post(`/api/v1/pages/${page.id}/share`, {
74 data: { scope: "public", password },
75 });
76 expect(shareRes.status()).toBe(201);
77 const share = (await shareRes.json()) as { share: { token: string } };
78
79 const anon = await browser.newContext();
80 const anonPage = await anon.newPage();
81 const publicUrl = `http://localhost:3000/s/${share.token}`;
82
83
84 await anonPage.goto(publicUrl);
85 const pw = anonPage.getByTestId("share-password-input");
86 await expect(pw).toBeVisible({ timeout: 20_000 });
87 await expect(anonPage.getByText("the secret is 42")).toHaveCount(0);
88
89
90 await pw.fill("wrong-password");
91 await anonPage.getByTestId("share-password-submit").click();
92 await expect(anonPage.getByTestId("share-password-error")).toBeVisible({
93 timeout: 10_000,
94 });
95 await expect(anonPage.getByText("the secret is 42")).toHaveCount(0);
96
97
98 await pw.fill(password);
99 await anonPage.getByTestId("share-password-submit").click();
100 await expect(anonPage.getByText("the secret is 42")).toBeVisible({
101 timeout: 15_000,
102 });
103 await anon.close();
104 });
105
106 test("expired share: link stops working after expiry", async ({
107 browser,
108 api,
109 freshWorkspace,
110 }) => {
111 const ws = await freshWorkspace("Expiry Space");
112 const page = await makePageWithContent(api, ws.id, "Ephemeral Report");
113
114
115 const expiresAt = new Date(Date.now() + 20_000).toISOString();
116 const shareRes = await api.post(`/api/v1/pages/${page.id}/share`, {
117 data: { scope: "public", expiresAt },
118 });
119 expect(shareRes.status()).toBe(201);
120 const share = (await shareRes.json()) as { share: { token: string } };
121 const publicUrl = `http://localhost:3000/s/${share.token}`;
122
123 const anon = await browser.newContext();
124 const anonPage = await anon.newPage();
125
126 await anonPage.goto(publicUrl);
127 await expect(anonPage.getByText("the secret is 42")).toBeVisible({
128 timeout: 20_000,
129 });
130
131 await anonPage.waitForTimeout(21_000);
132 await anonPage.reload();
133
134 await expect(anonPage.getByText(/expired|no longer available/i)).toBeVisible({
135 timeout: 15_000,
136 });
137 await expect(anonPage.getByText("the secret is 42")).toHaveCount(0);
138 await anon.close();
139 });
140
141 test("allowComments=false hides the comment UI on the public page", async ({
142 browser,
143 api,
144 freshWorkspace,
145 }) => {
146 const ws = await freshWorkspace("NoComment Space");
147 const page = await makePageWithContent(api, ws.id, "Quiet Report");
148
149 const shareRes = await api.post(`/api/v1/pages/${page.id}/share`, {
150 data: { scope: "public", allowComments: false },
151 });
152 const share = (await shareRes.json()) as { share: { token: string } };
153
154 const anon = await browser.newContext();
155 const anonPage = await anon.newPage();
156 await anonPage.goto(`http://localhost:3000/s/${share.token}`);
157 await expect(anonPage.getByTestId("public-page")).toBeVisible({ timeout: 20_000 });
158
159 await expect(anonPage.getByTestId("comment-popup")).toHaveCount(0);
160 await expect(anonPage.getByTestId("comment-input")).toHaveCount(0);
161 await expect(anonPage.getByText(/leave a comment|join the discussion/i)).toHaveCount(0);
162 await anon.close();
163 });
164
165 test("revoke share: link returns 404", async ({
166 browser,
167 api,
168 freshWorkspace,
169 }) => {
170 const ws = await freshWorkspace("Revoke Space");
171 const page = await makePageWithContent(api, ws.id, "Revoked Report");
172 const shareRes = await api.post(`/api/v1/pages/${page.id}/share`, {
173 data: { scope: "public" },
174 });
175 const share = (await shareRes.json()) as { share: { token: string; id?: string } };
176 const publicUrl = `http://localhost:3000/s/${share.token}`;
177
178
179 const revokeRes = await api.delete(`/api/v1/shares/${share.token}`);
180 expect([200, 204]).toContain(revokeRes.status());
181
182 const anon = await browser.newContext();
183 const anonPage = await anon.newPage();
184 const res = await anonPage.goto(publicUrl, { waitUntil: "domcontentloaded" });
185
186 const status = res?.status() ?? 200;
187 if (status === 404) {
188 await expect(anonPage.getByText(/not found|no longer available/i)).toBeVisible();
189 } else {
190 await expect(anonPage.getByTestId("public-page")).toHaveCount(0);
191 await expect(anonPage.getByText(/not found|no longer available|revoked/i)).toBeVisible({
192 timeout: 10_000,
193 });
194 }
195 await anon.close();
196 });
197
198 test("only the owner/allowed roles can publish a page", async ({
199 api,
200 secondUser,
201 freshWorkspace,
202 }) => {
203 const ws = await freshWorkspace("Permissioned Share Space");
204 const page = await createPage(api, ws.id, { title: "Not For Outsiders" });
205
206
207 const { api: outsiderApi } = await secondUser();
208 const denied = await outsiderApi.post(`/api/v1/pages/${page.id}/share`, {
209 data: { scope: "public" },
210 });
211 expect([403, 404]).toContain(denied.status());
212 });
213});
214