import { expect, test } from '@playwright/test'; import type { Page } from '@playwright/test'; import { contextForUser } from './helpers'; /** * Non-image attachments pack (issue #61): a page's attachments section uploads * an allowlisted file, lists it, and inserts it into the document as a * download link; a disallowed extension is rejected with the localized error; * the Pond Admin file manager reports usage and flags an orphan. Selectors are * language-neutral (the UI follows the user's locale) — CSS classes, not the * button text. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; const PDF = Buffer.from('%PDF-1.4 e2e attachment body'); async function createPage( context: Awaited>, title: string, ): Promise<{ pondSlug: string; pageSlug: string }> { const ponds = await context.request.get('/api/v1/ponds'); const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal'); const created = await context.request.post(`/api/v1/ponds/${pond.id}/pages`, { data: { title } }); const page = await created.json(); return { pondSlug: pond.slug, pageSlug: page.slug }; } async function openAttachments(page: Page): Promise { await page.getByRole('button', { name: /edit|bearbeiten/i }).click(); await expect(page.locator('.ProseMirror')).toHaveAttribute('contenteditable', 'true'); await page.locator('.editor-shell__attachments-toggle').click(); await expect(page.locator('.attachments-panel')).toBeVisible(); } test('uploads a page attachment, lists it, and inserts a download link', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Attach ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await openAttachments(page); await page.locator('.attachments-panel__input').setInputFiles({ name: 'report.pdf', mimeType: 'application/pdf', buffer: PDF, }); const item = page.locator('.attachments-item__name', { hasText: 'report.pdf' }); await expect(item).toBeVisible({ timeout: 10000 }); // Insert into the document as a link, then confirm it landed as an anchor. await page.locator('.attachments-item__insert').first().click(); const link = page.locator('.ProseMirror a[href^="/api/v1/media/"]'); await expect(link).toBeVisible(); // The linked media downloads (attachment disposition) with its filename and // is never rendered inline as HTML (ADR 0011, security.md §Uploads). const href = await link.getAttribute('href'); const served = await context.request.get(href!); expect(served.status()).toBe(200); expect(served.headers()['content-disposition']).toContain('attachment'); expect(served.headers()['content-disposition']).toContain('report.pdf'); expect(served.headers()['x-content-type-options']).toBe('nosniff'); await context.close(); }); test('rejects a disallowed extension with the localized error', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const { pondSlug, pageSlug } = await createPage(context, `E2E Reject ${Date.now()}`); const page = await context.newPage(); await page.goto(`/p/${pondSlug}/${pageSlug}`); await openAttachments(page); await page.locator('.attachments-panel__input').setInputFiles({ name: 'malware.exe', mimeType: 'application/octet-stream', buffer: Buffer.from('MZ not allowed'), }); await expect(page.locator('.attachments-panel .form-banner--error')).toBeVisible(); await expect(page.locator('.attachments-item__name')).toHaveCount(0); await context.close(); }); test('pond file manager shows usage and flags an orphan (Pond Admin)', async ({ browser }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const ponds = await context.request.get('/api/v1/ponds'); const pond = (await ponds.json()).find((p: { type: string }) => p.type === 'personal'); // A pond-level upload with no embedding page is an orphan candidate. const upload = await context.request.post(`/api/v1/ponds/${pond.id}/files`, { multipart: { file: { name: 'loose.pdf', mimeType: 'application/pdf', buffer: PDF } }, }); expect(upload.ok()).toBeTruthy(); const page = await context.newPage(); await page.goto(`/p/${pond.slug}/settings`); const manager = page.locator('.pond-file-manager'); await expect(manager).toBeVisible(); await expect(manager.locator('.pond-file-manager__usage')).toBeVisible(); const row = manager.locator('.attachments-item', { hasText: 'loose.pdf' }); await expect(row).toBeVisible(); await expect(row.locator('.attachments-item__orphan')).toBeVisible(); await context.close(); }); test('shows the classified-upload warning on a classified page (issue #213)', async ({ browser, }) => { const context = await contextForUser(browser, BASE_URL, 'fixture-user'); const page = await context.newPage(); await page.goto('/p/content-fixtures/classified-note'); await openAttachments(page); // The persistent warning names the consequence; the wording is the fixed // marking formula (ADR 0022), not localized. await expect(page.locator('.attachments-panel__warning')).toBeVisible(); await expect(page.locator('.attachments-panel__warning')).toContainText( 'VS – NUR FÜR DEN DIENSTGEBRAUCH', ); await context.close(); });