import { expect, test } from '@playwright/test'; import type { BrowserContext } from '@playwright/test'; import { contextForUser } from './helpers'; const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; /** * Public read access (issue #56): an anonymous visitor reads a page a `public` * grant opens, through the SPA's read-only view (no editor bundle), and its * embedded image streams too; a non-public page never resolves. Uses the seeded * `content-fixtures` pond (owned by fixture-user) whose "Fixture Image" page * carries a real servable image. */ async function pondId(owner: BrowserContext, slug: string): Promise { const res = await owner.request.get(`/api/v1/ponds/${slug}`); return ((await res.json()) as { id: string }).id; } test('an anonymous visitor reads a public page and its image via the SPA', async ({ browser }) => { const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const id = await pondId(owner, 'content-fixtures'); const grant = await owner.request.post(`/api/v1/ponds/${id}/grants`, { data: { subjectType: 'public', role: 'reader', scopeType: 'pond', effect: 'allow' }, }); const grantId = ((await grant.json()) as { id: string }).id; try { const anon = await browser.newContext({ baseURL: BASE_URL }); // no session const page = await anon.newPage(); // Pinned guarantee (#102): the anonymous/public read path never opens an // awareness/presence connection and never renders presence data. const collabSockets: string[] = []; page.on('websocket', (ws) => { if (ws.url().includes('/collab')) collabSockets.push(ws.url()); }); await page.goto('/public/content-fixtures/fixture-image'); // The read-only public view renders — with no collaborative editor. await expect(page.locator('.public-page__badge')).toBeVisible(); await expect(page.locator('.public-page__title')).toContainText('Fixture Image'); await expect(page.locator('.ProseMirror')).toHaveCount(0); await expect(page.locator('.presence-strip')).toHaveCount(0); await expect(page.locator('.presence-avatar')).toHaveCount(0); expect(collabSockets, `awareness sockets on the public path: ${collabSockets}`).toEqual([]); // The embedded image streams to the anonymous visitor (media honors public): // fetch its resolved /media URL from the same session-less context. const src = await page.locator('.public-page__body img').first().getAttribute('src'); expect(src).toMatch(/^\/api\/v1\/media\//); const media = await anon.request.get(src!); expect(media.status()).toBe(200); expect(media.headers()['content-type']).toContain('image/'); await anon.close(); } finally { await owner.request.delete(`/api/v1/ponds/${id}/grants/${grantId}`); await owner.close(); } }); test('a non-public page never resolves for an anonymous visitor', async ({ browser }) => { // A fresh private pond + page (no public grant) — isolated from any shared // fixture pond so the negative case cannot be polluted by another test. const owner = await contextForUser(browser, BASE_URL, 'fixture-user'); const pond = (await ( await owner.request.post('/api/v1/ponds', { data: { name: `Private ${Date.now()}` } }) ).json()) as { id: string; slug: string }; const page = (await ( await owner.request.post(`/api/v1/ponds/${pond.id}/pages`, { data: { title: 'Secret' } }) ).json()) as { slug: string }; const anon = await browser.newContext({ baseURL: BASE_URL }); const view = await anon.newPage(); // The SPA shows "not found"… await view.goto(`/public/${pond.slug}/${page.slug}`); await expect(view.locator('.public-page__body')).toHaveCount(0); // …and the content endpoint hides the page's existence. const res = await anon.request.get(`/api/v1/public/${pond.slug}/${page.slug}/content`); expect(res.status()).toBe(404); await anon.close(); await owner.close(); });