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()
+ })
+})