mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
test: add ProtectedRoute tests and remaining page tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
10524a471d
commit
edf30c02ec
3 changed files with 269 additions and 0 deletions
177
frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx
Normal file
177
frontend/src/components/layout/__tests__/ProtectedRoute.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
53
frontend/src/pages/__tests__/AccountPage.test.tsx
Normal file
53
frontend/src/pages/__tests__/AccountPage.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
39
frontend/src/pages/__tests__/ImportPage.test.tsx
Normal file
39
frontend/src/pages/__tests__/ImportPage.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue