Compare commits
3 Commits
7fbfeb3b39
...
f98792f10b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f98792f10b | ||
|
|
70d858b65a | ||
|
|
c1e82a7edf |
@@ -25,9 +25,9 @@ test.describe('Admin dashboard', () => {
|
||||
await page.goto('/admin');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Benutzer' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Gruppen' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Schlagworte' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Benutzer', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Gruppen', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Schlagworte', exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/admin-dashboard.png' });
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ test.describe('Admin — group management', () => {
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// Switch to the Groups tab
|
||||
await page.getByRole('button', { name: 'Gruppen' }).click();
|
||||
await page.getByRole('button', { name: 'Gruppen', exact: true }).click();
|
||||
|
||||
await page.getByPlaceholder('Gruppenname (z.B. Editoren)').fill('E2E Leser');
|
||||
|
||||
@@ -176,10 +176,14 @@ test.describe('Admin — tag management', () => {
|
||||
await page.goto('/admin');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.getByRole('button', { name: 'Schlagworte' }).click();
|
||||
await page.getByRole('button', { name: 'Schlagworte', exact: true }).click();
|
||||
// Wait for the tags list to render after the tab switch
|
||||
await page.waitForSelector('ul > li');
|
||||
|
||||
// Hover over the "Familie" row to reveal the opacity-0 action buttons
|
||||
const familieRow = page.locator('li').filter({ hasText: /^Familie$/ });
|
||||
const familieRow = page
|
||||
.locator('ul > li')
|
||||
.filter({ has: page.locator('span', { hasText: /^Familie$/ }) });
|
||||
await familieRow.hover();
|
||||
await familieRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
|
||||
|
||||
@@ -195,9 +199,12 @@ test.describe('Admin — tag management', () => {
|
||||
await page.goto('/admin');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await page.getByRole('button', { name: 'Schlagworte' }).click();
|
||||
await page.getByRole('button', { name: 'Schlagworte', exact: true }).click();
|
||||
await page.waitForSelector('ul > li');
|
||||
|
||||
const renamedRow = page.locator('li').filter({ hasText: /^Familie \(E2E\)$/ });
|
||||
const renamedRow = page
|
||||
.locator('ul > li')
|
||||
.filter({ has: page.locator('span', { hasText: /^Familie \(E2E\)$/ }) });
|
||||
await renamedRow.hover();
|
||||
await renamedRow.getByRole('button', { name: 'Schlagwort bearbeiten' }).click();
|
||||
|
||||
|
||||
@@ -49,11 +49,13 @@ test.describe('Authentication', () => {
|
||||
});
|
||||
|
||||
test('login establishes a session that authenticates API calls', async ({ page }) => {
|
||||
// Guards against regressions where the session cookie is set but broken —
|
||||
// a working URL redirect is not enough evidence that auth works end-to-end.
|
||||
// Guards against regressions where the session cookie is set but broken.
|
||||
// The profile page calls /api/users/me server-side — if auth works end-to-end,
|
||||
// it loads without redirecting to /login.
|
||||
await login(page);
|
||||
const response = await page.request.get('/api/users/me');
|
||||
expect(response.ok()).toBe(true);
|
||||
await page.goto('/profile');
|
||||
await expect(page).toHaveURL('/profile');
|
||||
await expect(page.getByRole('heading', { name: /Mein Profil/i })).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/auth-session-valid.png' });
|
||||
});
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ test.describe('Document creation', () => {
|
||||
await page.getByRole('button', { name: /Speichern/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||
await expect(page.getByText('E2E Testbrief')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'E2E Testbrief' })).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/document-create.png' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ test.describe('Person creation', () => {
|
||||
await page.getByRole('button', { name: /Erstellen/i }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/persons\/[^/]+$/);
|
||||
await expect(page.getByText('E2E Testperson')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'E2E Testperson' })).toBeVisible();
|
||||
await page.screenshot({ path: 'test-results/e2e/person-create.png' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,20 +72,19 @@ test.describe('Profile page', () => {
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
|
||||
await expect(page.getByText('Passwort erfolgreich geändert.')).toBeVisible();
|
||||
|
||||
// ── Step 2: navigate away — server session is invalidated ───────────────
|
||||
await page.goto('/');
|
||||
// After the password changes, the auth_token cookie still carries the old
|
||||
// credentials. use:enhance re-runs the page's load function, which calls
|
||||
// the backend with the stale Basic Auth header → 401 → redirect to /login.
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// ── Step 3: log in with the new password ───────────────────────────────
|
||||
// ── Step 2: log in with the new password ───────────────────────────────
|
||||
await page.getByLabel('Benutzername').fill('admin');
|
||||
await page.getByLabel('Passwort').fill('TempAdmin456!');
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
await page.screenshot({ path: 'test-results/e2e/profile-password-changed.png' });
|
||||
|
||||
// ── Step 4: restore the original password so subsequent tests still work ─
|
||||
// ── Step 3: restore the original password so subsequent tests still work ─
|
||||
await page.goto('/profile');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('input[name="currentPassword"]').fill('TempAdmin456!');
|
||||
@@ -95,11 +94,10 @@ test.describe('Profile page', () => {
|
||||
.locator('form[action*="changePassword"]')
|
||||
.getByRole('button', { name: /Speichern/i })
|
||||
.click();
|
||||
await expect(page.getByText('Passwort erfolgreich geändert.')).toBeVisible();
|
||||
|
||||
// ── Step 5: log back in with the restored password ─────────────────────
|
||||
await page.goto('/');
|
||||
// Redirected to /login again after credential rotation
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// ── Step 4: log back in with the restored password ─────────────────────
|
||||
await page.getByLabel('Benutzername').fill('admin');
|
||||
await page.getByLabel('Passwort').fill('admin123');
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
|
||||
@@ -27,6 +27,8 @@ const makeUser = (overrides = {}) => ({
|
||||
});
|
||||
|
||||
const baseData = {
|
||||
user: undefined,
|
||||
canWrite: true,
|
||||
users: [makeUser()],
|
||||
groups: [makeGroup()],
|
||||
tags: []
|
||||
@@ -38,35 +40,35 @@ afterEach(cleanup);
|
||||
|
||||
describe('Admin page – users tab', () => {
|
||||
it('shows the username in the table', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByRole('cell', { name: 'max', exact: true })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the full name in the table', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText(/Max Mustermann/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a dash when user has no name set', async () => {
|
||||
const data = { ...baseData, users: [makeUser({ firstName: undefined, lastName: undefined })] };
|
||||
render(Page, { data });
|
||||
render(Page, { data, form: null });
|
||||
await expect.element(page.getByText('–')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows group badges for the user', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText('Editoren')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('edit link points to /admin/users/[id]', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Bearbeiten/i }))
|
||||
.toHaveAttribute('href', '/admin/users/u1');
|
||||
});
|
||||
|
||||
it('new user button links to /admin/users/new', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Neuer Benutzer/i }))
|
||||
.toHaveAttribute('href', '/admin/users/new');
|
||||
@@ -74,7 +76,7 @@ describe('Admin page – users tab', () => {
|
||||
|
||||
it('shows "no groups" label when user has no groups', async () => {
|
||||
const data = { ...baseData, users: [makeUser({ groups: [] })] };
|
||||
render(Page, { data });
|
||||
render(Page, { data, form: null });
|
||||
await expect.element(page.getByText(/Keine Gruppen/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ const makeUser = (overrides = {}) => ({
|
||||
...overrides
|
||||
});
|
||||
|
||||
const baseData = { editUser: makeUser(), groups };
|
||||
const baseData = { user: undefined, canWrite: true, editUser: makeUser(), groups };
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -32,48 +32,48 @@ afterEach(cleanup);
|
||||
|
||||
describe('Admin edit user page – rendering', () => {
|
||||
it('renders the heading with username', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText(/Benutzer bearbeiten: max/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills first name from editUser data', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="firstName"]');
|
||||
expect(input?.value).toBe('Max');
|
||||
});
|
||||
|
||||
it('pre-fills last name from editUser data', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="lastName"]');
|
||||
expect(input?.value).toBe('Mustermann');
|
||||
});
|
||||
|
||||
it('pre-fills email from editUser data', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
|
||||
expect(input?.value).toBe('max@example.com');
|
||||
});
|
||||
|
||||
it('pre-fills birth date in German format (dd.mm.yyyy)', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const input = document.querySelector<HTMLInputElement>('input[placeholder="TT.MM.JJJJ"]');
|
||||
expect(input?.value).toBe('22.03.1985');
|
||||
});
|
||||
|
||||
it('pre-fills contact field', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const textarea = document.querySelector<HTMLTextAreaElement>('textarea[name="contact"]');
|
||||
expect(textarea?.value).toBe('Tel: 0123');
|
||||
});
|
||||
|
||||
it('renders group checkboxes', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText('Editoren')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Admins')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-selects the groups the user already belongs to', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const checkbox = document.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"][name="groupIds"][value="g1"]'
|
||||
);
|
||||
@@ -81,7 +81,7 @@ describe('Admin edit user page – rendering', () => {
|
||||
});
|
||||
|
||||
it('does not pre-select groups the user does not belong to', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const checkbox = document.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"][name="groupIds"][value="g2"]'
|
||||
);
|
||||
@@ -89,7 +89,7 @@ describe('Admin edit user page – rendering', () => {
|
||||
});
|
||||
|
||||
it('password fields are empty by default', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
const passwordInputs = document.querySelectorAll<HTMLInputElement>('input[type="password"]');
|
||||
passwordInputs.forEach((input) => {
|
||||
expect(input.value).toBe('');
|
||||
@@ -97,14 +97,14 @@ describe('Admin edit user page – rendering', () => {
|
||||
});
|
||||
|
||||
it('cancel link points to /admin', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Abbrechen/i }))
|
||||
.toHaveAttribute('href', '/admin');
|
||||
});
|
||||
|
||||
it('renders the save button', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByRole('button', { name: /Speichern/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ const groups = [
|
||||
{ id: 'g2', name: 'Admins', permissions: ['ADMIN'] }
|
||||
];
|
||||
|
||||
const baseData = { groups };
|
||||
const baseData = { user: undefined, canWrite: true, groups };
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
@@ -18,37 +18,37 @@ afterEach(cleanup);
|
||||
|
||||
describe('Admin new user page – rendering', () => {
|
||||
it('renders the page heading', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText(/Neuen Benutzer anlegen/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the login input', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByRole('textbox', { name: /Login/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders group checkboxes for each available group', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByText('Editoren')).toBeInTheDocument();
|
||||
await expect.element(page.getByText('Admins')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('cancel link points to /admin', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Abbrechen/i }))
|
||||
.toHaveAttribute('href', '/admin');
|
||||
});
|
||||
|
||||
it('back link points to /admin', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect
|
||||
.element(page.getByRole('link', { name: /Zurück/i }))
|
||||
.toHaveAttribute('href', '/admin');
|
||||
});
|
||||
|
||||
it('renders the create button', async () => {
|
||||
render(Page, { data: baseData });
|
||||
render(Page, { data: baseData, form: null });
|
||||
await expect.element(page.getByRole('button', { name: /Erstellen/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { createApiClient } from '$lib/api.server';
|
||||
import { parseBackendError, getErrorMessage } from '$lib/errors';
|
||||
@@ -16,7 +16,7 @@ export async function load({
|
||||
locals.user?.groups?.some((g: { permissions: string[] }) =>
|
||||
g.permissions.includes('WRITE_ALL')
|
||||
) ?? false;
|
||||
if (!canWrite) throw error(403, 'Forbidden');
|
||||
if (!canWrite) throw redirect(303, '/');
|
||||
|
||||
const senderId = url.searchParams.get('senderId') || '';
|
||||
const receiverId = url.searchParams.get('receiverId') || '';
|
||||
|
||||
Reference in New Issue
Block a user