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
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>
128 lines
5.4 KiB
TypeScript
128 lines
5.4 KiB
TypeScript
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();
|
||
});
|