mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 14:53:41 +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