dorfteich/apps/web/e2e/attachments.spec.ts
Claude Fable 5 868b79c8bc
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m15s
CI / Build container images (pull_request) Successful in 4m27s
CI / Auth e2e pack (pull_request) Successful in 9m10s
CI / Import/export fidelity gate (pull_request) Successful in 53s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 15s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 20s
CI / Lint, typecheck, test (push) Successful in 5m47s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m26s
CI / Import/export fidelity gate (push) Successful in 1m0s
#213: warn on uploads to classified pages; instance policy can block
The attachments panel of a classified page shows a persistent notice
naming the consequence (de+en): the file inherits the page's
classification but its content carries no marking (#212). The new
instance setting classification.uploadPolicy (default warn, documented;
the VS-NfD reference configuration blocks, #227) hardens the warning
into a server-side rejection (403 classified_upload_blocked) — enforced
in the upload service, not only in the UI. Tests: warning visible in the
local attachments pack; block enforced server-side with warn/block both
ways and open pages unaffected.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:33:34 +02:00

128 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<ReturnType<typeof contextForUser>>,
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<void> {
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();
});