Files
admin-frontend-domain/src/test/protect-auth-loading.test.tsx
T
naibaandcloudcode f07c557029 feat(api-tokens): add PAT management UI, CSRF handling, and auth-loading fixes
Add an API tokens management route to create, list, and revoke PATs, showing
the plaintext token once on creation with scope and server-id selection.

Mirror the nz-csrf cookie into the X-CSRF-Token header on unsafe fetcher
methods (POST/PUT/PATCH/DELETE) for the server-side double-submit check, and
self-heal expired sessions via refresh-token without a recursive fetch loop.

Gate protected routes behind resolved auth state to avoid pre-auth SWR fetches,
and fix the login loading/race so stale probes cannot clobber the session.

Add i18n keys for the new screens across all locales.

Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
2026-05-30 15:56:53 +00:00

109 lines
3.9 KiB
TypeScript

import { act, render } from "@testing-library/react"
import { MemoryRouter, Route, Routes } from "react-router-dom"
import { afterEach, beforeEach, expect, test, vi } from "vitest"
// ProtectedRoute must NOT mount its children while AuthProvider is still
// running the initial getProfile() probe — except for the /dashboard/login
// path itself. Mounting the protected subtree during the probe would fire
// authenticated SWR fetches like /api/v1/setting before auth is confirmed.
// Without this contract, a regression in protect.tsx silently re-introduces
// pre-auth traffic and a flash of protected UI before redirect.
let mockProfile: { id: number; role: number } | undefined
let mockLoading = false
vi.mock("@/hooks/useAuth", () => ({
useAuth: () => ({ profile: mockProfile, loading: mockLoading }),
}))
beforeEach(() => {
mockProfile = undefined
mockLoading = true
})
afterEach(() => {
document.body.innerHTML = ""
})
function renderAtPath(path: string, child: React.ReactNode) {
return import("@/routes/protect").then(({ default: ProtectedRoute }) => {
return act(async () => {
render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route
path="/dashboard/login"
element={
<ProtectedRoute>{child}</ProtectedRoute>
}
/>
<Route
path="/dashboard/*"
element={
<ProtectedRoute>{child}</ProtectedRoute>
}
/>
</Routes>
</MemoryRouter>,
)
})
})
}
test("ProtectedRoute does not mount children for protected paths while auth is loading", async () => {
await renderAtPath(
"/dashboard",
<div data-testid="protected-child">protected</div>,
)
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
})
test("ProtectedRoute renders children on the login page even while auth is loading", async () => {
await renderAtPath(
"/dashboard/login",
<div data-testid="login-child">login</div>,
)
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
})
test("ProtectedRoute redirects unauthenticated users without mounting protected children", async () => {
mockLoading = false
mockProfile = undefined
const { default: ProtectedRoute } = await import("@/routes/protect")
await act(async () => {
render(
<MemoryRouter initialEntries={["/dashboard"]}>
<Routes>
<Route
path="/dashboard/login"
element={
<ProtectedRoute>
<div data-testid="login-child">login</div>
</ProtectedRoute>
}
/>
<Route
path="/dashboard/*"
element={
<ProtectedRoute>
<div data-testid="protected-child">protected</div>
</ProtectedRoute>
}
/>
</Routes>
</MemoryRouter>,
)
})
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
})
test("ProtectedRoute renders children once an authenticated profile resolves", async () => {
mockLoading = false
mockProfile = { id: 1, role: 0 }
await renderAtPath(
"/dashboard",
<div data-testid="protected-child">protected</div>,
)
expect(document.querySelector("[data-testid='protected-child']")).not.toBeNull()
})