Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 31s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Failing after 26s
Sicherheit P-12 (MITT-05): logout() entfernt alle sj_coach_*-Schlüssel
aus sessionStorage gezielt per Präfix-Löschung. Fremde Schlüssel
(Browser-Extensions etc.) bleiben erhalten. Verhindert Datenleak bei
Nutzerwechsel im selben Tab (geteilter Rechner).
- AuthContext.jsx: Präfix-Schleife in logout()
- tests/dev-smoke-test.spec.js: Playwright-Test P-12 (injects/checks 3
sj_coach_*-Schlüssel + 1 Fremd-Schlüssel; prüft selektive Löschung)
Compliance-Dokumentation:
- docs/compliance-implementation.md: P-12 ✅, Version 0.8.68
- docs/compliance-package-register.md: kanonisches Paketregister (neu)
- docs/compliance-roadmap.md: lebende Steuerungs-Roadmap (neu)
- docs/compliance-audit.md: §20 Paket-ID-Stabilitätsregel
version: 0.8.68 (backend + frontend)
module: auth 1.2.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
7.6 KiB
JavaScript
210 lines
7.6 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
||
|
||
const TEST_EMAIL = process.env.TEST_EMAIL || 'lars@stommer.com';
|
||
const TEST_PASSWORD = process.env.TEST_PASSWORD || '12345678';
|
||
|
||
/** Primärer Submit auf der Login-Seite (nicht den Tab "Login" vs. "Registrieren"). */
|
||
async function submitLoginForm(page) {
|
||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||
}
|
||
|
||
async function login(page) {
|
||
await page.goto('/');
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Warte bis Login-Seite geladen ist
|
||
await page.waitForSelector('input[type="email"]', { timeout: 10000 });
|
||
|
||
await page.fill('input[type="email"]', TEST_EMAIL);
|
||
await page.fill('input[type="password"]', TEST_PASSWORD);
|
||
await submitLoginForm(page);
|
||
await page.waitForLoadState('networkidle');
|
||
}
|
||
|
||
test('1. Login funktioniert', async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForSelector('input[type="email"]', { timeout: 10000 });
|
||
await page.fill('input[type="email"]', TEST_EMAIL);
|
||
await page.fill('input[type="password"]', TEST_PASSWORD);
|
||
await submitLoginForm(page);
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Nach Login soll der Tab "Login" (Moduswahl) verschwinden — nicht der Submit "Anmelden"
|
||
const loginButton = page.locator('button:has-text("Login")');
|
||
await expect(loginButton).toHaveCount(0, { timeout: 10000 });
|
||
|
||
await page.screenshot({ path: 'screenshots/01-nach-login.png' });
|
||
console.log('✓ Login erfolgreich');
|
||
});
|
||
|
||
test('2. Dashboard lädt ohne Fehler', async ({ page }) => {
|
||
await login(page);
|
||
|
||
// Warte bis Spinner verschwunden
|
||
await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 });
|
||
|
||
// Dashboard: h1 „Dashboard“ + Begrüßungstext (nicht mehr „Willkommen bei Shinkan“ als Überschrift)
|
||
const main = page.locator('.app-main');
|
||
await expect(main.getByRole('heading', { level: 1, name: 'Dashboard' })).toBeVisible({
|
||
timeout: 5000,
|
||
});
|
||
await expect(main.getByText(/Shinkan unterstützt dich/i)).toBeVisible({ timeout: 5000 });
|
||
|
||
await page.screenshot({ path: 'screenshots/02-dashboard.png' });
|
||
console.log('✓ Dashboard OK');
|
||
});
|
||
|
||
test('3. Navigation zu Übungen', async ({ page }) => {
|
||
await login(page);
|
||
|
||
// Bei Viewport ≥1024px ist .bottom-nav versteckt — Mobile garantieren wie in playwright.config.js
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
|
||
// Desktop-Sidebar enthält ebenfalls Übungen – nur Mobile-Bottom-Nav klicken (sichtbarer Link)
|
||
await page.locator('.bottom-nav a[href="/exercises"]').click();
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Prüfe ob Übungen-Seite geladen
|
||
await expect(page.locator('h1, h2, .page-title')).toContainText(/übungen/i, { timeout: 5000 });
|
||
|
||
await page.screenshot({ path: 'screenshots/03-uebungen.png' });
|
||
console.log('✓ Übungen-Seite erreichbar');
|
||
});
|
||
|
||
test('4. Navigation zu Vereine', async ({ page }) => {
|
||
await login(page);
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
|
||
await page.locator('.bottom-nav a[href="/clubs"]').click();
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// ClubsPage: <h1>Vereinsverwaltung</h1> + Tab <h2>Vereine</h2> → ein kombinierter
|
||
// Selektor löst 2 Treffer aus (Playwright strict mode). URL + primäre Überschrift reichen.
|
||
await expect(page).toHaveURL(/\/clubs\/?$/, { timeout: 5000 });
|
||
await expect(page.getByRole('heading', { level: 1, name: /Vereinsverwaltung/i })).toBeVisible({
|
||
timeout: 5000,
|
||
});
|
||
|
||
await page.screenshot({ path: 'screenshots/04-vereine.png' });
|
||
console.log('✓ Vereine-Seite erreichbar');
|
||
});
|
||
|
||
test('5. Desktop-Sidebar sichtbar (Desktop)', async ({ page }) => {
|
||
// Desktop-Viewport
|
||
await page.setViewportSize({ width: 1280, height: 800 });
|
||
|
||
await login(page);
|
||
|
||
// Prüfe ob Desktop-Sidebar existiert
|
||
const sidebar = page.locator('.desktop-sidebar');
|
||
await expect(sidebar).toBeVisible({ timeout: 5000 });
|
||
|
||
await page.screenshot({ path: 'screenshots/05-desktop-sidebar.png' });
|
||
console.log('✓ Desktop-Sidebar sichtbar');
|
||
});
|
||
|
||
test('6. Bottom-Nav sichtbar (Mobile)', async ({ page }) => {
|
||
// Mobile-Viewport
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
|
||
await login(page);
|
||
|
||
// Prüfe ob Bottom-Nav existiert
|
||
const bottomNav = page.locator('.bottom-nav');
|
||
await expect(bottomNav).toBeVisible({ timeout: 5000 });
|
||
|
||
await page.screenshot({ path: 'screenshots/06-mobile-bottom-nav.png' });
|
||
console.log('✓ Bottom-Nav sichtbar');
|
||
});
|
||
|
||
test('7. Session-Persistenz nach Reload', async ({ page }) => {
|
||
await login(page);
|
||
|
||
await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 10000 });
|
||
await expect(
|
||
page.locator('.app-main').getByRole('heading', { level: 1, name: 'Dashboard' }),
|
||
).toBeVisible({ timeout: 10000 });
|
||
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Auth lädt erst nach Spinner – nicht auf /login stranden (stabiler als Button „Login“-Tab auf Login-Screen)
|
||
await expect(page.locator('.spinner')).toHaveCount(0, { timeout: 20000 });
|
||
await expect(page).not.toHaveURL(/\/login(?:\/|$|\?|#)/, { timeout: 20000 });
|
||
await expect(
|
||
page.locator('.app-main').getByRole('heading', { level: 1, name: 'Dashboard' }),
|
||
).toBeVisible({
|
||
timeout: 20000,
|
||
});
|
||
|
||
await page.screenshot({ path: 'screenshots/07-nach-reload.png' });
|
||
console.log('✓ Session bleibt nach Reload erhalten');
|
||
});
|
||
|
||
test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1280, height: 800 });
|
||
await login(page);
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Coach-Sitzungsdaten simulieren (wie sie TrainingCoachPage schreibt)
|
||
await page.evaluate(() => {
|
||
sessionStorage.setItem('sj_coach_step_42', '3');
|
||
sessionStorage.setItem('sj_coach_deltas_42', '[{"id":1}]');
|
||
sessionStorage.setItem('sj_coach_debrief_42', '0');
|
||
sessionStorage.setItem('fremd_anderer_key', 'muss_erhalten_bleiben');
|
||
});
|
||
|
||
const vorLogout = await page.evaluate(() => ({
|
||
step: sessionStorage.getItem('sj_coach_step_42'),
|
||
fremd: sessionStorage.getItem('fremd_anderer_key'),
|
||
}));
|
||
expect(vorLogout.step).toBe('3');
|
||
expect(vorLogout.fremd).toBe('muss_erhalten_bleiben');
|
||
|
||
await page.getByRole('button', { name: 'Abmelden' }).click();
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
const nachLogout = await page.evaluate(() => ({
|
||
step: sessionStorage.getItem('sj_coach_step_42'),
|
||
deltas: sessionStorage.getItem('sj_coach_deltas_42'),
|
||
debrief: sessionStorage.getItem('sj_coach_debrief_42'),
|
||
fremd: sessionStorage.getItem('fremd_anderer_key'),
|
||
token: localStorage.getItem('authToken'),
|
||
}));
|
||
|
||
expect(nachLogout.step).toBeNull();
|
||
expect(nachLogout.deltas).toBeNull();
|
||
expect(nachLogout.debrief).toBeNull();
|
||
expect(nachLogout.fremd).toBe('muss_erhalten_bleiben');
|
||
expect(nachLogout.token).toBeNull();
|
||
|
||
await page.screenshot({ path: 'screenshots/p12-session-storage-nach-logout.png' });
|
||
console.log('✓ P-12: sj_coach_* entfernt, Fremd-Key erhalten, authToken entfernt');
|
||
});
|
||
|
||
test('8. Keine kritischen Console-Fehler', async ({ page }) => {
|
||
const errors = [];
|
||
page.on('console', msg => {
|
||
if (msg.type() === 'error') errors.push(msg.text());
|
||
});
|
||
|
||
await login(page);
|
||
await page.waitForLoadState('networkidle');
|
||
|
||
// Filtere unkritische Fehler
|
||
const kritisch = errors.filter(e =>
|
||
!e.includes('favicon') &&
|
||
!e.includes('sourceMap') &&
|
||
!e.includes('404') &&
|
||
!e.includes('vite.svg')
|
||
);
|
||
|
||
if (kritisch.length > 0) {
|
||
console.log('⚠ Console-Fehler:', kritisch.join(', '));
|
||
} else {
|
||
console.log('✓ Keine kritischen Console-Fehler');
|
||
}
|
||
|
||
expect(kritisch.length).toBe(0);
|
||
});
|