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
- 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
86 lines
3.9 KiB
TypeScript
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();
|
|
});
|