Files
pablan/frontend/e2e/auth.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

108 lines
4.6 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { login, logout } from './helpers';
// Uses the seeded dev users (make seed): pablo@pablan.dev / pablan-dev.
async function gotoHydrated(page: Page, path: string) {
await page.goto(path);
await page.locator('body[data-hydrated]').waitFor();
}
async function signInOnCurrentPage(page: Page, email: string) {
// Fills the login form already on screen — NO page.goto. A full
// navigation would reset the module singletons and hide the very leak
// this exercises. Wait for hydration first, so the client submit handler
// (which does the full-reload navigation) is wired up. Selectors are by
// attribute, not label: the logged-out login page is rendered in
// whatever language the previous user left, which is part of the point.
await page.locator('body[data-hydrated]').waitFor();
await page.locator('input[name="email"]').fill(email);
await page.locator('input[name="password"]').fill('pablan-dev');
await page.locator('input[name="password"]').press('Enter');
await expect(page).toHaveURL('/');
await page.locator('body[data-hydrated]').waitFor();
}
test('redirects anonymous visitors to the login page', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveURL(/\/login$/);
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
});
test('shows an error for wrong credentials', async ({ page }) => {
await gotoHydrated(page, '/login');
await page.getByLabel('Email').fill('pablo@pablan.dev');
await page.getByLabel('Password').fill('definitely-wrong');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toHaveText('Email or password is incorrect.');
await expect(page).toHaveURL(/\/login$/);
});
test('login and logout round-trip', async ({ page }) => {
await gotoHydrated(page, '/login');
await page.getByLabel('Email').fill('pablo@pablan.dev');
await page.getByLabel('Password').fill('pablan-dev');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/');
await expect(page.getByTestId('sidebar').getByText('Pablo')).toBeVisible();
// The landing greets by first name and invites capture.
await expect(page.getByRole('heading', { name: /Pablo/ })).toBeVisible();
await logout(page);
await page.goto('/');
await expect(page).toHaveURL(/\/login$/);
});
// Regression for the P0 session-state leak: switching users in one browser
// context must not carry the previous user's conversation titles (an
// information disclosure) or their interface language across the boundary.
// Deliberately drives the real in-app flow — logout button, then the login
// form on the page it lands on — with no page.goto between users, which is
// what masked this in the other specs.
const MARKER = 'Zebrafrage-Session-P0-Test';
let leakConversationId: string | null = null;
test.afterEach(async ({ page }) => {
// End whatever session is current (max's UI login is never logged out by
// the test itself), then re-auth as pablo to undo his residue.
await page.request.post('/api/auth/logout');
await page.request.post('/api/auth/login', {
data: { email: 'pablo@pablan.dev', password: 'pablan-dev' }
});
await page.request.put('/api/account/locale', { data: { locale: null } });
if (leakConversationId) {
await page.request.delete(`/api/conversations/${leakConversationId}`);
leakConversationId = null;
}
await page.request.post('/api/auth/logout');
});
test('a different user does not inherit the previous session state', async ({ page }) => {
// Pablo: German interface and one conversation with a recognizable title.
await login(page, 'pablo@pablan.dev');
await page.request.put('/api/account/locale', { data: { locale: 'de' } });
const created = await page.request.post('/api/conversations', { data: { mode: 'query' } });
leakConversationId = (await created.json()).id;
await page.request.post(`/api/conversations/${leakConversationId}/messages`, {
data: { content: MARKER }
});
// Reload (pablo → pablo) so the German setting and the new conversation
// are on screen before the switch.
await page.goto('/');
await page.locator('body[data-hydrated]').waitFor();
await expect(page.getByTestId('sidebar')).toContainText(MARKER);
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
// Switch to Max through the UI, no page.goto.
await logout(page);
await page.locator('body[data-hydrated]').waitFor();
await signInOnCurrentPage(page, 'max@pablan.dev');
// Max sees only his own world.
await expect(page.getByTestId('sidebar')).not.toContainText(MARKER);
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
});