From edf30c02ec411cd64ad1bcc732c75d1b05fda4d7 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 21:40:25 +0000 Subject: [PATCH] test: add ProtectedRoute tests and remaining page tests Co-Authored-By: Claude Opus 4.6 --- .../layout/__tests__/ProtectedRoute.test.tsx | 177 ++++++++++++++++++ .../src/pages/__tests__/AccountPage.test.tsx | 53 ++++++ .../src/pages/__tests__/ImportPage.test.tsx | 39 ++++ 3 files changed, 269 insertions(+) create mode 100644 frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx create mode 100644 frontend/src/pages/__tests__/AccountPage.test.tsx create mode 100644 frontend/src/pages/__tests__/ImportPage.test.tsx diff --git a/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx b/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx new file mode 100644 index 0000000..f6f39ea --- /dev/null +++ b/frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx @@ -0,0 +1,177 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { render, screen, waitFor } from '@testing-library/react' +import { Routes, Route, MemoryRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { mockUser, mockDakUser } from '@/test/mocks/data' +import { AuthProvider } from '@/context/AuthContext' +import { ProtectedRoute } from '@/components/layout/ProtectedRoute' + +/** + * Helper: renders a mini app with Routes so we can detect redirects. + * ProtectedRoute uses , so we set up + * a /login route that renders identifiable text. + */ +function renderWithRoutes( + options: { + initialRoute?: string + requireAdmin?: boolean + } = {}, +) { + const { initialRoute = '/protected', requireAdmin = false } = options + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false, gcTime: 0 }, + mutations: { retry: false }, + }, + }) + + return render( + + + + + +
Protected Content
+ + } + /> + Login Page} /> +
+
+
+
, + ) +} + +describe('ProtectedRoute', () => { + beforeEach(() => { + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + }) + + it('redirects to /login when not authenticated (no token)', async () => { + // No token in localStorage -> AuthProvider sets isLoading=false, user=null immediately + // ProtectedRoute sees !user -> + server.use( + http.get('/api/auth/me', () => { + return new HttpResponse(null, { status: 401 }) + }), + ) + + renderWithRoutes() + + await waitFor(() => { + expect(screen.getByText('Login Page')).toBeInTheDocument() + }) + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument() + }) + + it('redirects to /login when token exists but /auth/me returns 401', async () => { + // Token is set but API rejects it (expired, invalid) + localStorage.setItem('access_token', 'expired-token') + + server.use( + http.get('/api/auth/me', () => { + return new HttpResponse(null, { status: 401 }) + }), + ) + + renderWithRoutes() + + await waitFor(() => { + expect(screen.getByText('Login Page')).toBeInTheDocument() + }) + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument() + }) + + it('shows children when authenticated as admin', async () => { + localStorage.setItem('access_token', 'valid-token') + + // Default handler returns mockUser (admin) — no override needed + + renderWithRoutes() + + await waitFor(() => { + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) + expect(screen.queryByText('Login Page')).not.toBeInTheDocument() + }) + + it('shows children when authenticated as dak_mitarbeiter (no requireAdmin)', async () => { + localStorage.setItem('access_token', 'valid-token') + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json(mockDakUser) + }), + ) + + renderWithRoutes({ requireAdmin: false }) + + await waitFor(() => { + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) + expect(screen.queryByText('Login Page')).not.toBeInTheDocument() + }) + + it('blocks non-admin users when requireAdmin is true', async () => { + localStorage.setItem('access_token', 'valid-token') + + server.use( + http.get('/api/auth/me', () => { + return HttpResponse.json(mockDakUser) + }), + ) + + renderWithRoutes({ requireAdmin: true }) + + await waitFor(() => { + expect(screen.getByText('Zugriff verweigert')).toBeInTheDocument() + }) + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument() + expect(screen.queryByText('Login Page')).not.toBeInTheDocument() + }) + + it('allows admin users when requireAdmin is true', async () => { + localStorage.setItem('access_token', 'valid-token') + + // Default handler returns mockUser (role: 'admin') + + renderWithRoutes({ requireAdmin: true }) + + await waitFor(() => { + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) + expect(screen.queryByText('Zugriff verweigert')).not.toBeInTheDocument() + }) + + it('shows loading state while auth is checking', async () => { + localStorage.setItem('access_token', 'valid-token') + + // Delay the /auth/me response to observe the loading state + server.use( + http.get('/api/auth/me', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return HttpResponse.json(mockUser) + }), + ) + + renderWithRoutes() + + // ProtectedRoute shows "Laden..." while isLoading is true + expect(screen.getByText('Laden...')).toBeInTheDocument() + expect(screen.queryByText('Protected Content')).not.toBeInTheDocument() + expect(screen.queryByText('Login Page')).not.toBeInTheDocument() + + // After the delayed response resolves, the protected content should appear + await waitFor(() => { + expect(screen.getByText('Protected Content')).toBeInTheDocument() + }) + expect(screen.queryByText('Laden...')).not.toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/__tests__/AccountPage.test.tsx b/frontend/src/pages/__tests__/AccountPage.test.tsx new file mode 100644 index 0000000..68eb442 --- /dev/null +++ b/frontend/src/pages/__tests__/AccountPage.test.tsx @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import { renderWithProviders } from '@/test/utils' +import { AccountPage } from '@/pages/AccountPage' + +describe('AccountPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders the page title and description', async () => { + renderWithProviders(, { initialRoute: '/account' }) + + await waitFor(() => { + expect(screen.getByText('Kontoverwaltung')).toBeInTheDocument() + }) + + expect( + screen.getByText(/Verwalten Sie Ihr Profil, Ihre Sicherheitseinstellungen/), + ).toBeInTheDocument() + }) + + it('shows profile, security, and MFA tabs', async () => { + renderWithProviders(, { initialRoute: '/account' }) + + await waitFor(() => { + expect(screen.getByText('Profil')).toBeInTheDocument() + }) + + expect(screen.getByText('Sicherheit')).toBeInTheDocument() + expect(screen.getByText('Zwei-Faktor')).toBeInTheDocument() + }) + + it('shows profile form fields in the profile tab', async () => { + renderWithProviders(, { initialRoute: '/account' }) + + // Wait for auth to resolve and render the profile tab (default) + await waitFor(() => { + expect(screen.getByLabelText('Vorname')).toBeInTheDocument() + }) + + expect(screen.getByLabelText('Nachname')).toBeInTheDocument() + expect(screen.getByLabelText('Anzeigename')).toBeInTheDocument() + expect(screen.getByLabelText('Benutzername')).toBeInTheDocument() + expect(screen.getByLabelText('E-Mail')).toBeInTheDocument() + + // The "Speichern" button should be visible + expect(screen.getByRole('button', { name: /Speichern/ })).toBeInTheDocument() + + // The avatar upload button should be visible + expect(screen.getByText('Bild hochladen')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/__tests__/ImportPage.test.tsx b/frontend/src/pages/__tests__/ImportPage.test.tsx new file mode 100644 index 0000000..9d6f058 --- /dev/null +++ b/frontend/src/pages/__tests__/ImportPage.test.tsx @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { screen, waitFor } from '@testing-library/react' +import { renderWithProviders } from '@/test/utils' +import { ImportPage } from '@/pages/ImportPage' + +describe('ImportPage', () => { + beforeEach(() => { + localStorage.setItem('access_token', 'test-token') + }) + + it('renders the page title', async () => { + renderWithProviders(, { initialRoute: '/import' }) + + await waitFor(() => { + expect(screen.getByText('Import')).toBeInTheDocument() + }) + }) + + it('shows CSV and ICD import tabs', async () => { + renderWithProviders(, { initialRoute: '/import' }) + + await waitFor(() => { + expect(screen.getByText('CSV-Import')).toBeInTheDocument() + }) + + expect(screen.getByText('ICD-Import')).toBeInTheDocument() + expect(screen.getByText('Import-Verlauf')).toBeInTheDocument() + }) + + it('shows the CSV upload drop zone by default', async () => { + renderWithProviders(, { initialRoute: '/import' }) + + await waitFor(() => { + expect(screen.getByText('CSV-Datei hierhin ziehen')).toBeInTheDocument() + }) + + expect(screen.getByText('oder klicken zum Auswählen')).toBeInTheDocument() + }) +})