Files
pablan/frontend/e2e/permissions.spec.ts
T
ProfessorNovaandClaude Opus 5 97dbff309c Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 08:36:17 +02:00

68 lines
2.7 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { login as loginAs, logout } from './helpers';
// Permission boundary in the browser, against the restricted corpus
// document "Wissenssicherung: Werner Krause" (visibility: restricted,
// granted to Engineering only). Pablo (Engineering) sees it, Max (Sales)
// must not — in the list, in the detail view, and in chat sources.
test.setTimeout(120_000);
async function login(page: Page, email: string) {
await loginAs(page, email);
}
test('restricted corpus document stays invisible across list, detail and chat', async ({
page
}) => {
// Pablo (Engineering, has the grant) can see it — and we grab the id.
await login(page, 'pablo@pablan.dev');
const fromApi = (await (await page.request.get('/api/documents?search=Wissenssicherung')).json())
.items;
expect(fromApi.length).toBeGreaterThan(0);
const restrictedId = fromApi[0].id;
await page.goto('/documents');
await page.locator('body[data-hydrated]').waitFor();
await page.getByTestId('document-search').fill('Wissenssicherung');
await expect(page.getByTestId('document-list')).toContainText('Wissenssicherung: Werner Krause', {
timeout: 10_000
});
await logout(page);
// Max (Sales): list is empty, detail 404s, chat cites nothing restricted.
await login(page, 'max@pablan.dev');
await page.goto('/documents');
await page.locator('body[data-hydrated]').waitFor();
await page.getByTestId('document-search').fill('Wissenssicherung Werner Krause');
// Search ranks by meaning, so it returns related documents rather than
// nothing — what matters is that the restricted one is never among them.
await expect(
page.getByTestId('document-list').or(page.getByText('No documents match'))
).toBeVisible({
timeout: 15_000
});
await expect(page.locator('body')).not.toContainText('Wissenssicherung: Werner Krause');
await page.goto(`/documents/${restrictedId}`);
await expect(page.getByText('Document not found')).toBeVisible();
await page.getByTestId('sidebar-new-conversation').click();
await page
.getByTestId('chat-input')
.fill('Welcher Servotec-Techniker kennt die F-350 am besten?');
await page.getByTestId('send-button').click();
await expect(page.getByTestId('send-button')).toBeVisible({ timeout: 60_000 });
const sources = page.getByTestId('sources');
if ((await sources.count()) > 0) {
await expect(sources.last()).not.toContainText('Wissenssicherung');
}
// Cleanup: ben's chat question created a conversation; drop it + session.
const conversations = await (await page.request.get('/api/conversations')).json();
if (conversations.length > 0) {
await page.request.delete(`/api/conversations/${conversations[0].id}`);
}
await page.request.post('/api/auth/logout');
});