dorfteich/apps/web/e2e/public.spec.ts
Claude Fable 5 e740ea6c01
All checks were successful
CD / Build and push images (push) Successful in 1m39s
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m33s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 46s
Move live presence into the TopBar, signed-in only (#102)
- the TopBar registers a presence slot (only rendered for signed-in
  users) next to the page-actions slot; PageEditor portals the
  PresenceStrip into it — behavior unchanged (initials avatars, max 5 +
  overflow, viewer badge, hidden when empty, both view and edit mode)
- pinned guarantee: public.spec asserts the anonymous read path opens no
  /collab websocket and renders no presence data

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 04:42:59 +02:00

86 lines
3.9 KiB
TypeScript

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<string> {
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();
});