import { test, expect } from '@playwright/test'; import { pasta } from '../fixtures'; import { exportRecipes } from '../../src/lib/import'; const headers = { origin: 'http://localhost:3100' }; test('authorization, CSRF, optimistic concurrency, validation and transactional imports', async ({ request, }) => { expect((await request.post('/api/recipes', { headers, data: pasta })).status()).toBe(401); expect( ( await request.post('/api/auth/login', { headers: { origin: 'https://evil.invalid' }, data: { username: 'testadmin', password: process.env.TEST_PASSWORD }, }) ).status(), ).toBe(403); const login = await request.post('/api/auth/login', { headers, data: { username: 'testadmin', password: process.env.TEST_PASSWORD }, }); expect(login.ok()).toBe(true); expect(login.headers()['set-cookie']).toMatch(/HttpOnly/i); expect(login.headers()['set-cookie']).toMatch(/SameSite=strict/i); const created = await request.post('/api/recipes', { headers, data: { ...pasta, id: undefined, title: 'API Rezept', published: false }, }); expect(created.status()).toBe(201); const r = await created.json(); expect( ( await request.put(`/api/recipes/${r.id}`, { headers, data: { ...r, title: 'API Rezept geändert' }, }) ).status(), ).toBe(200); expect( ( await request.put(`/api/recipes/${r.id}`, { headers, data: { ...r, title: 'Veraltete Änderung' }, }) ).status(), ).toBe(409); expect( (await request.put(`/api/recipes/${r.id}`, { headers, data: { ...r, servings: 0 } })).status(), ).toBe(422); expect( ( await request.post('/api/import', { headers, data: { format: 'json', preview: false, raw: exportRecipes( [{ ...pasta, id: undefined, title: 'Transaktion darf nicht bleiben' }, pasta], 'json', ), }, }) ).status(), ).toBe(409); expect( (await (await request.get('/api/recipes?admin=1')).json()).some( (r: { title: string }) => r.title === 'Transaktion darf nicht bleiben', ), ).toBe(false); expect( ( await request.post('/api/import', { headers, data: { format: 'xml', preview: true, raw: ']>', }, }) ).status(), ).toBe(422); expect( ( await request.post('/api/media', { headers: { ...headers, 'content-type': 'image/svg+xml' }, data: '', }) ).status(), ).toBe(415); expect( ( await request.post('/api/media', { headers: { ...headers, 'content-type': 'image/png' }, data: 'not a png', }) ).status(), ).toBe(422); await request.post('/api/auth/logout', { headers }); expect((await request.get(`/api/recipes/${r.id}`)).status()).toBe(404); expect((await request.get('/api/export')).status()).toBe(401); expect((await request.delete(`/api/recipes/${r.id}`, { headers })).status()).toBe(401); }); test('password change revokes other sessions and brute force is rate limited', async ({ request, playwright, }) => { const other = await playwright.request.newContext({ baseURL: 'http://localhost:3100' }); const credentials = { username: 'testadmin', password: process.env.TEST_PASSWORD }; await request.post('/api/auth/login', { headers, data: credentials }); await other.post('/api/auth/login', { headers, data: credentials }); const replacement = `${process.env.TEST_PASSWORD}-changed`; expect( ( await request.post('/api/auth/password', { headers, data: { current: process.env.TEST_PASSWORD, password: replacement }, }) ).status(), ).toBe(200); expect((await other.get('/api/export')).status()).toBe(401); expect( ( await request.post('/api/auth/password', { headers, data: { current: replacement, password: process.env.TEST_PASSWORD }, }) ).status(), ).toBe(200); for (let n = 0; n < 8; n++) expect( ( await other.post('/api/auth/login', { headers, data: { username: 'nonexistent', password: 'wrong' }, }) ).status(), ).toBe(401); expect( ( await other.post('/api/auth/login', { headers, data: { username: 'nonexistent', password: 'wrong' }, }) ).status(), ).toBe(429); await other.dispose(); });