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'); });