test: add ProtectedRoute tests and remaining page tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-26 21:40:25 +00:00
parent 10524a471d
commit edf30c02ec
3 changed files with 269 additions and 0 deletions

View file

@ -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 <Navigate to="/login" replace />, 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(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialRoute]}>
<AuthProvider>
<Routes>
<Route
path="/protected"
element={
<ProtectedRoute requireAdmin={requireAdmin}>
<div>Protected Content</div>
</ProtectedRoute>
}
/>
<Route path="/login" element={<div>Login Page</div>} />
</Routes>
</AuthProvider>
</MemoryRouter>
</QueryClientProvider>,
)
}
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 -> <Navigate to="/login" />
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()
})
})

View file

@ -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(<AccountPage />, { 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(<AccountPage />, { 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(<AccountPage />, { 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()
})
})

View file

@ -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(<ImportPage />, { initialRoute: '/import' })
await waitFor(() => {
expect(screen.getByText('Import')).toBeInTheDocument()
})
})
it('shows CSV and ICD import tabs', async () => {
renderWithProviders(<ImportPage />, { 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(<ImportPage />, { initialRoute: '/import' })
await waitFor(() => {
expect(screen.getByText('CSV-Datei hierhin ziehen')).toBeInTheDocument()
})
expect(screen.getByText('oder klicken zum Auswählen')).toBeInTheDocument()
})
})